Slint for .NET

Community .NET bindings for the Slint UI toolkit, in preview.

yurkinh.slint.net 1.19.0-preview.12 · .NET 10

Community project. These bindings and this page are a community project, not affiliated with or endorsed by SixtyFPS GmbH, the makers of Slint. For the Slint language itself, see the official Slint documentation. Report problems with the .NET bindings on github.com/yurkinh/slint.

Overview

Slint is a declarative UI toolkit: you describe the UI in .slint files and write the application logic in another language. These bindings let you write that logic in C# on .NET 10.

The packages are yurkinh.slint.net and yurkinh.slint.net.Compiler, which the first one pulls in. Slint for .NET is a preview: its APIs may still change.

For a tour of what the bindings can do, read Slint ♥ .NET.

Quick Start

Install the .NET 10 SDK.

With a Template

From 1.19.0-preview.10.

dotnet new install yurkinh.slint.net.Templates
dotnet new slint -o MyApp
cd MyApp
dotnet run

The template creates a ready app:

The package has five templates:

TemplateCreates
slintA desktop app for macOS, Linux, and Windows.
slint-androidAn Android app; needs the .NET Android workload.
slint-iosAn iOS app; needs the .NET iOS workload and a Mac.
slint-webA web app, which runs in the browser; needs the wasm-tools workload. From 1.19.0-preview.11.
slint-xplatA solution with a desktop, an Android, and an iOS project that share the UI and the logic.

--app-id com.example.myapp sets the Android package name and the iOS bundle ID. slint-xplat puts the UI in ui/, the C# that the platforms share in Shared/, and the settings that they share in Directory.Build.props. Each project generates its own classes from the .slint files. Without the Android or iOS workload, pick the projects with --platforms:

dotnet new slint-xplat -o MyApp --platforms desktop --platforms android

--platforms web adds a web project, which isn't in the defaults, since it needs the wasm-tools workload.

By Hand

  1. Create a project and add the package:

    dotnet new console -o MyApp
    cd MyApp
    dotnet add package yurkinh.slint.net --prerelease
  2. Create ui/app-window.slint:

    import { Button, VerticalBox } from "std-widgets.slint";
    
    export component AppWindow inherits Window {
        in-out property <int> counter: 42;
        callback request-increase-value();
        VerticalBox {
            Text {
                text: "Counter: \{root.counter}";
            }
            Button {
                text: "Increase value";
                clicked => {
                    root.request-increase-value();
                }
            }
        }
    }
  3. Add it to MyApp.csproj:

    <ItemGroup>
      <SlintFile Include="ui/app-window.slint" />
    </ItemGroup>
  4. Replace Program.cs:

    using MyApp;
    
    internal static class Program
    {
        // Windows needs this for the window, see Threading.
        [STAThread]
        private static void Main()
        {
            using var app = AppWindow.Create();
            app.OnRequestIncreaseValue(() => app.Counter++);
            app.Run();
        }
    }
  5. Run it with dotnet run.

Project Setup

Each <SlintFile> item in the project file generates C# code at build time. Mark files that another <SlintFile> only imports with Generate="false":

<ItemGroup>
  <SlintFile Include="ui/app-window.slint" />
  <SlintFile Include="ui/widgets.slint" Generate="false" />
</ItemGroup>

These MSBuild properties change the generated code:

PropertyMeaning
SlintNamespaceThe namespace of the generated classes, instead of the root namespace.
SlintStyleThe widget style, such as fluent, material, or cupertino.
SlintCopyToOutputfalse loads the .slint files from the project directory instead of copying them to the output.
SlintEmbedFilestrue embeds the .slint files in the assembly, with the images and fonts they use, instead of copying them, see Deployment. On by default for a single-file publish, and on Android and iOS. From 1.19.0-preview.12.
SlintPrecompilefalse compiles the .slint files when the app starts. From 1.19.0-preview.12, the build compiles them by default, and the app starts without compiling them again.
SlintIncludePathsDirectories that imports search, separated by semicolons.
SlintLivePreviewtrue reloads the UI when a .slint file changes, see Hot Reload.
SlintTranslationDomain, SlintBundledTranslationsBundle translations, see Translations.

For imports from a library, such as import { Button } from "@mylib/button.slint", add a library path, relative to the project:

<ItemGroup>
  <SlintLibraryPath Include="mylib" Path="../mylib/" />
</ItemGroup>

The build compiles the .slint files, with the images and fonts they use, and embeds the result in the assembly, so the app starts without compiling them: the gallery example creates its window in 17 ms instead of 106 ms on the desktop, and in 165 ms instead of 385 ms in the browser. The build also copies the .slint files to the output directory, next to the application. The app compiles them at startup instead with live reload, or with SLINT_STYLE set to a style other than the build's.

Project Files and Editions

A slint-project.json sets the Slint edition of a project, its main file, and its include and library paths. The edition decides which language fixes apply, such as layout fixes from Slint 1.19. To use one, add the project directory instead of a file:

<ItemGroup>
  <SlintProject Include="ui" />
</ItemGroup>
{
    "slint-edition": "1.19",
    "main": "app-window.slint",
    "include-paths": ["shared"]
}

The build generates the classes of the main file, and copies the project file and the .slint files under the directory. The application compiles with the project's edition and paths, both at build time and at run time, and live reload works the same way. Use <SlintProject Include="." /> for a project file next to the .csproj.

SlintIncludePaths and <SlintLibraryPath> don't apply to a project, since its project file has its own paths. A <SlintFile> compiles without a project file, and so without an edition.

Generated Code

For each <SlintFile>, the build generates C# types in the project's root namespace:

SlintC#
exported componenta class with a static Create() method, plus Show(), Hide(), Run(), and Window
globala class, available as a property of each component
in, out, and in-out propertiesproperties; out properties are read-only
functiona method
callback clickedOnClicked(handler) to set the handler, and InvokeClicked(...) to call it
structa record with init properties
enuman enum

Names become PascalCase: request-increase-value becomes RequestIncreaseValue. A component exported under another name too, such as with export { AppWindow as MainWindow }, gets a class for each name.

The types map as follows:

SlintC#
intint
float, length, angle, percentfloat
durationTimeSpan
stringstring
boolbool
colorSlint.Color
brushSlint.Brush; a Slint.Color converts to it
PointSlint.LogicalPosition
imageSlint.Image
keysSlint.Keys
styled-textSlint.StyledText
data-transferSlint.DataTransfer
[T]Slint.Model<T>
built-in enums, such as TextWrapSlint.EnumValue

Numbers convert to integers with saturation and truncation, like in Slint. When you rebuild after changing a .slint file, the generated classes change with it; at run time, the generated code checks that the file still matches, and throws a SlintException if it doesn't.

Generating the Code by Hand

The build runs slint-compiler for each <SlintFile>. Run it yourself to generate the code in a script, or for a project that doesn't use the package's MSBuild targets:

slint-compiler -f csharp ui/app-window.slint -o AppWindow.g.cs \
    --csharp-namespace MyApp \
    --csharp-runtime-path ui/app-window.slint

The other settings map to options:

SettingOption
SlintStyle--style fluent
SlintIncludePaths-I <dir>, once per directory
<SlintLibraryPath>-L mylib=../mylib/
SlintTranslationDomain--translation-domain <domain>
SlintBundledTranslations--bundle-translations <dir>
SlintNoDefaultTranslationContext--no-default-translation-context
SlintLivePreview--csharp-live-preview

The package yurkinh.slint.net.Compiler has the compiler in tools/<rid>/, such as ~/.nuget/packages/yurkinh.slint.net.compiler/<version>/tools/osx-arm64/slint-compiler. NuGet doesn't keep its executable bit, so run chmod +x on it first. Add the generated file to the project with <Compile Include="AppWindow.g.cs" />, and generate it again when the .slint file changes. To build with a slint-compiler of your own instead, set SlintCompilerPath to it.

Properties and Callbacks

Given this component:

export component Calculator inherits Window {
    in-out property <int> total;
    out property <bool> is-negative: total < 0;
    callback add(int) -> int;
    callback reset();
    public function describe() -> string {
        return "Total: \{total}";
    }
}

the generated Calculator class works like this:

using var calculator = Calculator.Create();

calculator.Total = 10;
bool negative = calculator.IsNegative;   // out properties are read-only

// A callback with arguments and a return value takes a Func.
calculator.OnAdd(value => calculator.Total += value);
calculator.OnReset(() => calculator.Total = 0);

int result = calculator.InvokeAdd(5);    // calls the handler, like Slint does
string text = calculator.Describe();     // public functions are methods

Setting a handler replaces the previous one. An exception that a handler throws stops the event loop, and Run() throws it again. Outside the event loop, the call that ran the handler throws it, such as InvokeAdd.

Structs and Enums

Structs become records, and enums become C# enums:

export enum Priority { low, normal, high }

export struct TodoItem {
    title: string,
    done: bool,
    priority: Priority = Priority.normal,
}
var item = new TodoItem { Title = "Write the docs" };
// Fields start with the defaults that the struct declares.
Console.WriteLine(item.Priority);        // Normal

// Records are immutable; use `with` to change a field.
var finished = item with { Done = true };

Records compare by value. To change a row of a model, replace it with a modified copy, as in Models.

Globals

An exported global becomes a class, which each component has as a property:

export global Logic {
    in-out property <int> the-value;
    pure callback magic-operation(int) -> int;
}

export component App inherits Window {
    Text { text: Logic.magic-operation(Logic.the-value); }
}
using var app = App.Create();
app.Logic.OnMagicOperation(value => value * 2);
app.Logic.TheValue = 42;

Models

Use a ListModel<T> for the rows of a for repeater, or any array property. Slint updates the UI whenever you change the list:

var todos = new ListModel<TodoItem>();
app.Todos = todos;
todos.Add(new TodoItem { Title = "Write the docs" });
todos[0] = todos[0] with { Done = true };

To show your own data, derive from Model<T>, and call NotifyRowChanged, NotifyRowAdded, NotifyRowRemoved, or NotifyReset when it changes. Any IEnumerable<T> also converts to a model, as a copy.

Filter, Sort, Map, and Reverse return views of a model that follow its changes:

app.OpenTodos = todos.Filter(todo => !todo.Done).Sort((a, b) => a.Title.CompareTo(b.Title));

Filter and Sort return a FilterModel and a SortModel. SourceRow maps a row of the view to its row in the source, such as to edit the item that the user selected, and Reset applies a condition that changed outside the model, such as a search text:

var prefix = "";
var shown = names.Filter(name => name.StartsWith(prefix));
app.OnPrefixEdited(text => { prefix = text; shown.Reset(); });
app.OnDeleteClicked(() => names.RemoveAt(shown.SourceRow(app.CurrentItem)));

Reading an array property gives back the model you set, or a read-only model for an array from the .slint file.

View Models

From 1.19.0-preview.12.

Bind a view model, such as one written with CommunityToolkit.Mvvm, to a component, instead of copying values between them. Add [SlintViewModel] with the class of the component, and call the Bind that the build generates:

[SlintViewModel(typeof(AppWindow))]
public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private string _name = "";

    public ObservableCollection<string> Tags { get; } = [];

    [RelayCommand(CanExecute = nameof(CanSave))]
    private void Save() => Tags.Add(Name);

    private bool CanSave() => Name.Length > 0;
}

using var app = AppWindow.Create();
using var binding = app.Bind(new MainViewModel());
app.Run();

Bind binds the members of the same C# name:

The build warns about a member whose type doesn't fit. For other names and conversions, bind members one by one through the component's Instance:

using var binding = app.Instance.Bind(viewModel)
    .Property("title", vm => vm.Title)
    .Property("note", vm => vm.Note, (vm, value) => vm.Note = value)
    .Command("submit", vm => vm.SubmitCommand, enabledProperty: "can-submit");

[SlintViewModel(typeof(SettingsPage))] binds a view model to an exported global instead, with app.SettingsPage.Bind(viewModel), such as one global per page. The view model can raise its changes on any thread; the component updates on the Slint thread. dotnet new slint --mvvm creates an app with a view model, and examples/mvvm/dotnet binds the view models of the MVVM-Samples of CommunityToolkit.Mvvm.

Without the attribute, ObservableCollectionModel<T> turns a collection into a model that follows its changes, and WatchProperty of ComponentInstance runs code after a property changes.

Images

Image.Load loads an image file, and Image.LoadFromData decodes a PNG, JPEG, or SVG in memory, such as a download. Image.FromRgba8 and Image.FromRgb8 create an image from pixels that you computed, and ToRgba8 returns the pixels of an image:

var tile = Image.LoadFromData(await http.GetByteArrayAsync(url), "png");
var pixels = original.ToRgba8();
Invert(pixels);
app.Preview = Image.FromRgba8(original.Width, original.Height, pixels);

From 1.19.0-preview.10, Image.FromPremultipliedRgba8 takes pixels whose colors are already multiplied by their alpha, as SkiaSharp draws them, such as an SKBitmap's GetPixelSpan(). FromRgba8 would show them too dark wherever they're semi-transparent.

Charts With Microcharts

Needs 1.19.0-preview.10 or later.

Microcharts draws bar, line, point, donut, pie, radar, and gauge charts with SkiaSharp, for MAUI and the native iOS and Android views. Microcharts.Slint, in the slint branch of the fork until Microcharts takes it in, shows them in a Slint Image: a ChartRenderer draws the chart at the element's size in physical pixels, and hands the pixels to Image.FromPremultipliedRgba8.

SlintTimer.Install(); // before creating charts, so that they animate on the Slint thread
using var app = AppWindow.Create();
var renderer = new ChartRenderer(app.Window, image => app.SalesChart = image)
{
    Chart = new BarChart { Entries = entries },
};
app.OnSalesChartResized(renderer.Resize);
app.Run();

The .slint side is a ChartView, an Image that reports its size, from chart-view.slint. The chart's sizes, such as LabelTextSize, are in logical pixels, like Slint's lengths. The gallery sample runs on the desktop, Android, and iOS:

Microcharts on Slint on Android: a bar chart, a point chart, and a line chart in white cards
Android, on a Pixel 9 emulator.
Microcharts on Slint on iOS: the same gallery in the iPhone simulator
iOS, in the iPhone simulator.

Window

Each component has a Window, for the window it shows in:

app.Window.LogicalSize = new LogicalSize(800, 600);
app.Window.IsMaximized = true;
app.Window.OnCloseRequested(() =>
    HasUnsavedChanges() ? CloseRequestResponse.KeepWindowShown : CloseRequestResponse.HideWindow);

It has the position and size in logical and physical pixels, the scale factor, IsFullscreen, IsMaximized, and IsMinimized, and Show, Hide, and RequestRedraw.

DispatchEvent sends input to the window as if it came from the windowing system, such as the keys of an on-screen keyboard:

app.VirtualKeyboardHandler.OnKeyPressed(key =>
{
    app.Window.DispatchEvent(new WindowEvent.KeyPressed(key));
    app.Window.DispatchEvent(new WindowEvent.KeyReleased(key));
});

A component that inherits SystemTrayIcon has no window: its class has Show and Hide, which show and hide the icon, but no Window or Run.

Drawing With OpenGL

From 1.19.0-preview.9.

Draw with OpenGL below or on top of the scene, or into a texture that an Image shows. Require a renderer that uses OpenGL before any other use of Slint, then set a rendering notifier on the window:

new BackendSelector().RequireOpenGL().Select();
using var app = AppWindow.Create();
app.Window.SetRenderingNotifier((state, api) =>
{
    switch (state)
    {
        case RenderingState.RenderingSetup:
            // Load the OpenGL functions, such as with api.GetProcAddress("glDrawArrays"), and create resources.
            break;
        case RenderingState.BeforeRendering:
            // Draw below the scene, or into a texture:
            // app.Texture = Image.FromBorrowedOpenGLTexture(texture, width, height);
            break;
        case RenderingState.RenderingTeardown:
            // Release the resources.
            break;
    }
});
app.Run();
The OpenGL underlay example: an animated grid of diamonds that a shader draws below Slint's text and checkbox
opengl_underlay: a shader draws below the scene.
The OpenGL texture example: a lit blue cube in a Slint Image, above sliders for its color
opengl_texture: a cube rendered into a texture that an Image shows.

Timers and Async Code

Use Slint.Timer to run code after a delay, or repeatedly:

using var timer = new Slint.Timer();
timer.Start(TimerMode.Repeated, TimeSpan.FromSeconds(1), () => app.Counter++);

Slint.Timer.SingleShot(TimeSpan.FromMilliseconds(500), () => app.Status = "Ready");

The name is Slint.Timer, since System.Threading.Timer is also in scope with implicit usings. While the event loop runs, await continues on the Slint thread, so async callbacks can update properties after awaiting:

app.OnRefresh(async () =>
{
    var data = await httpClient.GetStringAsync(url);
    app.Status = data;
});

From another thread, call EventLoop.InvokeFromEventLoop to run code on the Slint thread. To run an async main function, call EventLoop.Run(async () => { ... }), which returns when the function completes.

Threading

Slint objects belong to the thread that first uses Slint. Use them from that thread only, and make it the main thread: the one that runs Main. macOS requires the event loop on the main thread.

On Windows, mark Main with [STAThread]. The windowing backend sets up OLE for drag and drop, which needs a single-threaded apartment. Without one, Create and Run throw an InvalidOperationException. Top-level statements can't take the attribute, so write Main out, as in the Quick Start. The attribute does nothing on macOS and Linux, so keep it for every platform.

Don't make Main async, since an async Main runs in a multithreaded apartment. For asynchronous code, call EventLoop.Run(async () => { ... }) from a synchronous Main.

Hot Reload

Change the UI and the code of a running application without restarting it: .slint files reload through Slint's live reload, and C# code through .NET Hot Reload.

.slint Files

Turn on live reload for Debug builds in your project file:

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
  <SlintLivePreview>true</SlintLivePreview>
</PropertyGroup>

The generated code then loads the .slint files from the project directory, and each component reloads in its window when you save a file it uses. The properties and callbacks you set, models included, carry over to the new version. A file with errors keeps the previous version until you fix it. To reload a single file, set LivePreview="true" on its <SlintFile> instead. ComponentInstance.Reloaded tells you when a component reloaded.

C# Code

Run dotnet watch, or start the application in the debugger of Visual Studio, VS Code with C# Dev Kit, or Rider, and apply changes with the IDE's Hot Reload command. A handler you passed to an On… method runs the new code from its next call.

Both at Once

With SlintLivePreview on, run dotnet watch. Saving a .slint file reloads the UI, and saving a .cs file applies the C# change, both without losing the state of the application.

What Needs a Restart

Use live reload during development only.

Translations

Mark strings for translation with @tr() in .slint files. To bundle gettext .po files into the application, set:

<PropertyGroup>
  <SlintTranslationDomain>myapp</SlintTranslationDomain>
  <SlintBundledTranslations>translations</SlintBundledTranslations>
</PropertyGroup>

The build bundles translations/<language>/LC_MESSAGES/myapp.po. After creating a component, switch the language at run time:

Translations.SelectBundledTranslation("de");

An empty string selects the strings as written in the .slint file. To translate with your own code instead, implement ITranslator and pass it to Translations.SetTranslator.

Without Generated Code

Compiler compiles .slint files at run time, and ComponentInstance accesses properties, callbacks, and functions by name. Use it for UIs that you only know at run time, such as plug-ins or user-provided files:

using var compiler = new Compiler();
using var result = compiler.BuildFromPath("app-window.slint");
if (result.HasErrors)
{
    foreach (var diagnostic in result.Diagnostics)
    {
        Console.Error.WriteLine(diagnostic);
    }
    return;
}

using var definition = result.Component("AppWindow")!;
using var app = definition.Create();
app.SetCallback("request-increase-value", _ => app.SetProperty("counter", (double)app.GetProperty("counter")! + 1));
app.Run();

Untyped values are double for numbers, string, bool, Slint.Struct for structs, Slint.EnumValue for enums, and Slint.Model<object?> for arrays. BuildFromSource compiles a string, and Compiler has the style, include paths, and library paths as properties.

Lifetime and Errors

Components, timers, images, and compilation results hold native resources; dispose them, such as with using. Slint holds the delegates you pass to On… methods, so a handler that captures its own component keeps it alive until you dispose the component.

Failures in Slint throw SlintException, such as a .slint file that doesn't compile at run time, a generated class that no longer matches its file, or a property that doesn't exist.

Android

From 1.19.0-preview.7.

The package also runs on Android 8 (API level 26) and later, on arm64-v8a and x86_64 devices and emulators. Slint draws into a NativeActivity and runs its event loop on a thread of its own, as in the C++ API.

From 1.19.0-preview.10, dotnet new slint-android -o MyApp creates the app in one step, see With a Template. Then dotnet build -t:Run installs it on the running emulator or the connected device. To set up the project yourself:

  1. Install the .NET Android workload, then create a project and add the package:

    dotnet workload install android
    dotnet new android -o MyApp
    cd MyApp
    dotnet add package yurkinh.slint.net --prerelease
  2. In MyApp.csproj, change SupportedOSPlatformVersion to 26, add the runtime identifiers that Slint has libraries for, and add your .slint file:

    <PropertyGroup>
      <SupportedOSPlatformVersion>26</SupportedOSPlatformVersion>
      <RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
    </PropertyGroup>
    <ItemGroup>
      <SlintFile Include="ui/app-window.slint" />
    </ItemGroup>

    The template already sets SupportedOSPlatformVersion to 24: change that line rather than adding a second one.

  3. Create ui/app-window.slint, the same as in the Quick Start:

    import { Button, VerticalBox } from "std-widgets.slint";
    
    export component AppWindow inherits Window {
        in-out property <int> counter: 42;
        callback request-increase-value();
        VerticalBox {
            Text {
                text: "Counter: \{root.counter}";
            }
            Button {
                text: "Increase value";
                clicked => {
                    root.request-increase-value();
                }
            }
        }
    }

    Without a <SlintFile> item for it, the build generates no AppWindow class, and fails with "The name 'AppWindow' does not exist".

  4. Replace MainActivity.cs: derive the main activity from SlintActivity, and create the components in SlintMain:

    using Slint.Android;
    
    namespace MyApp;
    
    [Activity(MainLauncher = true, ConfigurationChanges = SlintActivity.ConfigurationChanges)]
    [MetaData(SlintActivity.LibraryNameKey, Value = SlintActivity.LibraryName)]
    public class MainActivity : SlintActivity
    {
        protected override void SlintMain()
        {
            using var app = AppWindow.Create();
            app.OnRequestIncreaseValue(() => app.Counter++);
            app.Run();
        }
    }

    Both attributes are needed, since .NET Android only reads them from your own activity. The template's Resources/layout/activity_main.xml is no longer used, and you can delete it.

  5. Connect a device with USB debugging, or start an emulator, and run the app:

    dotnet build -t:Run

    To install the APK of a Debug build with adb install instead, set <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>, since a Debug build leaves the assemblies out of the APK otherwise.

Keeping State

From 1.19.0-preview.8.

Android also ends the process of an app in the background when it needs memory, and starts a new one when the user returns. To keep what the user was doing, return it from SlintSaveState, and take it back in SlintRestoreState:

protected override byte[]? SlintSaveState() => JsonSerializer.SerializeToUtf8Bytes(settings);

protected override void SlintRestoreState(byte[] state) => Apply(JsonSerializer.Deserialize<Settings>(state));

SlintRestoreState runs once, in the event loop, after SlintMain created the components. SlintPause, SlintResume, and SlintLowMemory report the other activity events, all on Slint's thread, unlike OnPause and the other methods of Activity. Android drops the saved state when the user closes the app, so keep what must outlast that in files or preferences.

See the todo example for Android, which shares its logic with the desktop version and keeps its list this way.

iOS

From 1.19.0-preview.10.

The package also runs on iOS 15 and later, with Skia on Metal, on devices and in the simulator; it's tested on an iPhone 13. As on Android, Slint owns the app's entry point: there's no AppDelegate and no UIApplication.Main. dotnet new slint-ios -o MyApp --app-id com.example.myapp creates the app, see With a Template. To set up the project yourself:

  1. Install the .NET iOS workload, with dotnet workload install ios, and create a project for net10.0-ios with your .slint file:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net10.0-ios</TargetFramework>
        <SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
        <OutputType>Exe</OutputType>
        <ApplicationId>com.example.myapp</ApplicationId>
      </PropertyGroup>
      <ItemGroup>
        <SlintFile Include="ui/app-window.slint" />
      </ItemGroup>
      <ItemGroup>
        <PackageReference Include="yurkinh.slint.net" Version="1.19.0-preview.12" />
      </ItemGroup>
    </Project>
  2. Add an Info.plist that names Slint's scene delegate, which iOS 27 requires, like the one of the todo example:

    <key>UIApplicationSceneManifest</key>
    <dict>
      <key>UIApplicationSupportsMultipleScenes</key>
      <false/>
      <key>UISceneConfigurations</key>
      <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
          <dict>
            <key>UISceneConfigurationName</key>
            <string>Slint</string>
            <key>UISceneDelegateClassName</key>
            <string>SlintWindowSceneDelegate</string>
          </dict>
        </array>
      </dict>
    </dict>
  3. Create the components in Main, and call Run, where Slint starts the app and doesn't return:

    internal static class Program
    {
        private static void Main()
        {
            using var app = AppWindow.Create();
            app.OnRequestIncreaseValue(() => app.Counter++);
            app.Run();
        }
    }
  4. Build and start it in the simulator, or with -r ios-arm64 and a signing identity on a device:

    dotnet build -t:Run

Web

From 1.19.0-preview.11.

The package also runs in the browser, on .NET's WebAssembly runtime, with the same C# API and generated code as on the desktop. Slint renders into a <canvas> with FemtoVG on WebGL 2, through the Emscripten backend that Slint's C++ API uses for the web. dotnet new slint-web -o MyApp creates the app, and dotnet new slint-xplat --platforms web adds a web project to a solution, see With a Template. To set up the project yourself:

  1. Install the workload that links Slint into .NET's WebAssembly runtime, with dotnet workload install wasm-tools, and create a project with the WebAssembly SDK and your .slint file:

    <Project Sdk="Microsoft.NET.Sdk.WebAssembly">
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <OutputType>Exe</OutputType>
      </PropertyGroup>
      <ItemGroup>
        <SlintFile Include="ui/app-window.slint" />
        <PackageReference Include="yurkinh.slint.net" Version="1.19.0-preview.12" />
      </ItemGroup>
    </Project>
  2. Add wwwroot/index.html with a canvas with the id canvas, and start .NET:

    <canvas id="canvas"></canvas>
    <script type="module">
      import { dotnet } from "./_framework/dotnet.js";
      await dotnet.run();
    </script>

    Without a size, the canvas takes the window's preferred size. With a size in its style attribute, such as style="width: 100vw; height: 100vh", the window follows the page.

  3. Await RunAsync instead of calling Run, since the browser's thread can't block:

    using var app = AppWindow.Create();
    app.OnRequestIncreaseValue(() => app.Counter++);
    await app.RunAsync();

    The task completes when EventLoop.Quit stops the loop, and fails with the exception of a callback. Run throws in the browser. On the desktop, RunAsync runs like Run and returns a completed task, so code that the platforms share can await it.

  4. dotnet run serves the app, and dotnet publish -c Release writes a trimmed build to bin/Release/net10.0/publish/wwwroot, which any static web server can host.

Limits:

Deployment

Publish the application for a runtime identifier, which picks the native library of that platform:

dotnet publish -c Release -r osx-arm64
dotnet publish -c Release -r win-x64 --self-contained
dotnet publish -c Release -r linux-x64 -p:PublishAot=true

The packages have native binaries for these runtime identifiers:

PlatformRuntime identifiers
macOSosx-arm64, osx-x64
Linuxlinux-x64, linux-arm64
Windowswin-x64, win-arm64
Android, from 1.19.0-preview.7android-arm64, android-x64
The browser, from 1.19.0-preview.11browser-wasm, as a static library that the app links

Embedded Linux

On a device without a desktop, such as a board with a touchscreen, Slint draws straight to the display with its linuxkms backend, and reads input with libinput. Publish for the device's runtime identifier, and start the app on its console:

dotnet publish -c Release -r linux-arm64 --self-contained
SLINT_BACKEND=linuxkms ./MyApp

We tested it on an arm64 Kali Linux virtual machine in UTM on a Mac, with the samples published from the package: every sample runs with the software renderer, typing and clicking work through libinput, and with the fix of 1.19.0-preview.10, the OpenGL renderers and the OpenGL samples render too. See Slint's LinuxKMS guide for the renderers and display settings.

The printer demo on the Linux console, drawn by the linuxkms backend with the software renderer
The printer demo, with the software renderer.
The todo example on the Linux console, with an entry typed through libinput
The todo example, after typing an entry through libinput.
The OpenGL underlay example on the Linux console, rendered with Skia's OpenGL renderer
The OpenGL underlay, with Skia's OpenGL renderer.

Building From Source

Build the native library, the compiler, and the packages yourself for a platform or a change that isn't published. This needs Rust and a clone of the dotnet_bindings branch. From the repository root, on an Apple Silicon Mac:

# The native library and the compiler for this machine
cargo build --release -p slint-dotnet -p slint-compiler

# The native library for Android devices, with the NDK's compilers
cargo build --release -p slint-dotnet --target aarch64-linux-android

# One directory per runtime identifier
mkdir -p out/native/osx-arm64 out/native/android-arm64 out/compiler/osx-arm64
cp target/release/libslint_dotnet.dylib out/native/osx-arm64/
cp target/aarch64-linux-android/release/libslint_dotnet.so out/native/android-arm64/
cp target/release/slint-compiler out/compiler/osx-arm64/

cd api/dotnet
dotnet pack Slint.Compiler -c Release -o ../../out/packages -p:Version=1.19.0-local.1 \
    -p:SlintCompilerBinariesDir=$PWD/../../out/compiler
dotnet pack Slint -c Release -o ../../out/packages -p:Version=1.19.0-local.1 \
    -p:SlintNativeRuntimesDir=$PWD/../../out/native -p:SlintAndroid=true
PlatformNative libraryCompiler
macOSlibslint_dotnet.dylibslint-compiler
Linuxlibslint_dotnet.soslint-compiler
Windowsslint_dotnet.dllslint-compiler.exe
Androidlibslint_dotnet.so, from target/<triple>/releasenone

Environment Variables

VariableMeaning
SLINT_BACKENDSelects the windowing backend, such as winit, or linuxkms on embedded Linux.
SLINT_ENABLE_EXPERIMENTAL_FEATURESEnables experimental Slint language features, at build time and at run time, since the generated code compiles the .slint files again. Up to 1.19.0-preview.7, 1 enables all of them. From 1.19.0-preview.8, list the features by name, separated by commas, such as match,closures; 1 is then an error.
SLINT_DOTNET_NATIVE_LIBRARYThe path of the native library to load instead of the one in the package, such as a local build.

Troubleshooting

InvalidOperationException about a single-threaded apartment on Windows

Mark Main with [STAThread], and replace top-level statements and async Main with a synchronous Main, see Threading.

"Slint objects can only be used from the thread that first used Slint"

A Task.Run or a thread pool callback touched a component. Use EventLoop.InvokeFromEventLoop to get back to the Slint thread.

"is out of date; regenerate it"

The .slint file next to the application doesn't match the generated code. Rebuild, and ship the .slint files from the same build.

An Android app closes right after it starts

Run adb logcat -s Slint: SlintActivity logs an exception of SlintMain under that tag before the activity ends.

"Error adding gbm buffer as framebuffer" on embedded Linux

The display driver doesn't take buffer modifiers, such as virtio-gpu in a virtual machine. Run with SLINT_BACKEND=linuxkms-software, or update to 1.19.0-preview.10, see Embedded Linux.

"Could not create a WebGL 2 context" in the browser

The browser has WebGL 2 turned off, or no GPU it allows. Use the software renderer, see Web.

"The browser's thread can't block: use RunAsync"

Replace Run with await RunAsync() in the web project, see Web.

The window doesn't open on Linux

Install libxkbcommon-x11, and check that DISPLAY or WAYLAND_DISPLAY is set.

Samples

Most examples and demos of Slint have a C# version in their dotnet directory, next to the Rust, C++, Node.js, and Python ones. Run one with dotnet run --project <directory>:

The carousel, speedometer, food processor, and analog clock have C# versions too, and the viewer shows the examples that have no logic, such as the layouts.

Releases

What's New lists the changes in each preview. The packages on nuget.org: yurkinh.slint.net. The source is in the dotnet_bindings branch.