yurkinh.slint.net
1.19.0-preview.13
dotnet add package yurkinh.slint.net --version 1.19.0-preview.13
NuGet\Install-Package yurkinh.slint.net -Version 1.19.0-preview.13
<PackageReference Include="yurkinh.slint.net" Version="1.19.0-preview.13" />
<PackageVersion Include="yurkinh.slint.net" Version="1.19.0-preview.13" />
<PackageReference Include="yurkinh.slint.net" />
paket add yurkinh.slint.net --version 1.19.0-preview.13
#r "nuget: yurkinh.slint.net, 1.19.0-preview.13"
#:package yurkinh.slint.net@1.19.0-preview.13
#addin nuget:?package=yurkinh.slint.net&version=1.19.0-preview.13&prerelease
#tool nuget:?package=yurkinh.slint.net&version=1.19.0-preview.13&prerelease
This is a community build of the Slint .NET bindings, not published by the Slint project.
Slint for .NET (Preview)
Slint is a UI toolkit that supports different programming languages. Slint for .NET is the integration with C# and .NET 10.
Warning Slint for .NET is a preview: its APIs may still change.
What's New
See What's New for the changes in each preview.
Documentation
The Slint for .NET documentation covers more than this README, such as deployment, troubleshooting, and samples. It's community documentation, not affiliated with SixtyFPS GmbH.
Prerequisites
- .NET 10 SDK
- macOS, Linux, or Windows, on x64 or arm64, embedded Linux without a desktop, Android on arm64 and x86_64, or iOS
Quick Start
From 1.19.0-preview.10, a template creates the app in one step:
dotnet new install yurkinh.slint.net.Templates
dotnet new slint -o MyApp
cd MyApp
dotnet run
The app has the counter below, live preview in Debug builds for dotnet watch, and Native AOT in Release builds.
The desktop projects are WinExe, so Windows doesn't open a console window for them, see step 3 below.
--style fluent, material, cupertino, or cosmic picks the widget style.
dotnet new slint --mvvm binds the window to a view model of CommunityToolkit.Mvvm, see View Models.
| Template | Creates |
|---|---|
slint |
A desktop app for macOS, Linux, and Windows. |
slint-android |
An Android app. |
slint-ios |
An iOS app. |
slint-web |
A web app, which runs in the browser. |
slint-xplat |
A 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.
dotnet new 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.
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 needs the wasm-tools workload.
Or set up the project yourself:
Create a project and add the
yurkinh.slint.netpackage:dotnet new console -o MyApp cd MyApp dotnet add package yurkinh.slint.net --prereleaseCreate
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(); } } } }Add it to
MyApp.csproj, and changeOutputTypetoWinExe:<PropertyGroup> <OutputType>WinExe</OutputType> </PropertyGroup> <ItemGroup> <SlintFile Include="ui/app-window.slint" /> </ItemGroup>With
Exe, Windows opens a console window next to the app's window.WinExehas no console, like a WPF or Avalonia app, and is the same asExeon macOS and Linux. On Windows, the app's console output, such as Slint'sdebug()and live reload errors, then goes nowhere. KeepExewhile you need it.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(); } }Run it with
dotnet run.
Generated Code
For each <SlintFile>, the build generates C# classes in the project's root namespace:
| Slint | C# |
|---|---|
| exported component | a class with a static Create() method, plus Show(), Hide(), Run(), and RunAsync() |
| global | a class, available as a property of each component |
in, out, and in-out properties |
properties; out properties are read-only |
| function | a method |
callback clicked |
OnClicked(handler) to set the handler, and InvokeClicked(...) to call it |
| struct | a record with init properties |
| enum | an 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.
Structs and enums keep only their own name.
To run code after a property changes, such as one that the user edits, watch it through the component's Instance:
using var watch = app.Instance.WatchProperty("counter", () => Console.WriteLine(app.Counter));
Slint checks once per frame, so several changes between two frames call the handler once.
WatchGlobalProperty watches a property of a global.
The types map as follows:
| Slint | C# |
|---|---|
int |
int |
float, length, angle, percent |
float |
duration |
TimeSpan |
string |
string |
bool |
bool |
color |
Slint.Color |
brush |
Slint.Brush; a Slint.Color converts to it |
Point |
Slint.LogicalPosition |
image |
Slint.Image |
keys |
Slint.Keys |
styled-text |
Slint.StyledText |
data-transfer |
Slint.DataTransfer |
[T] |
Slint.Model<T> |
Color and Brush have Brighter, Darker, Transparentize, and WithAlpha, and Color has Mix.
Create an Image from a file with Image.Load, from a file in memory with Image.LoadFromData,
or from pixels that you computed with Image.FromRgba8 and Image.FromRgb8.
For pixels whose colors are already multiplied by their alpha, as SkiaSharp draws them, use Image.FromPremultipliedRgba8.
Charts With Microcharts
Microcharts draws bar, line, point, donut, pie, radar, and gauge charts with SkiaSharp.
Its Microcharts.Slint, in the slint branch of the fork until Microcharts takes it in,
shows them in a Slint Image with a ChartRenderer, on the desktop, Android, and iOS.
Its README has the code, and Sources/Microcharts.Samples.Slint a gallery; both need 1.19.0-preview.10 or later.
ToRgba8 returns the pixels.
The build copies the .slint files to the output directory, and the generated code loads them at run time.
Mark files that another <SlintFile> imports with Generate="false":
<SlintFile Include="ui/app-window.slint" />
<SlintFile Include="ui/widgets.slint" Generate="false" />
Set these properties to change the generated code:
SlintNamespace: the namespace, instead of the root namespace.SlintStyle: the widget style, such asfluentorcupertino.SlintCopyToOutput:falseto load the.slintfiles from the project directory instead of copying them.SlintEmbedFiles:trueto embed the.slintfiles in the assembly instead of copying them, see Publishing. It's on by default on Android and iOS.SlintPrecompile:falseto compile the.slintfiles when the app starts. From 1.19.0-preview.12, the build compiles them by default, and the app starts without compiling them again, such as about 90 ms sooner for the gallery example on the desktop, and 220 ms sooner in the browser. WithSLINT_STYLEset to a style other than the build's, the app compiles them at startup.SlintIncludePaths: directories that imports search, separated by semicolons.
To use a project file, add its directory instead of a file:
<SlintProject Include="ui" />
The build generates the classes of the project's main file, and copies the project file and the .slint files under the directory.
The application then compiles with the edition and the paths of ui/slint-project.json, at build and at run time.
SlintIncludePaths and <SlintLibraryPath> don't apply to a project, since its project file has its own paths.
For imports from a library, such as import { Button } from "@mylib/button.slint", add a library path:
<SlintLibraryPath Include="mylib" Path="../mylib/" />
Paths are relative to the project.
The generated code loads imports from these directories where they are,
so an application that runs elsewhere needs the imported files copied, such as with <SlintFile Generate="false" />.
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 Slint.targets:
slint-compiler -f csharp ui/app-window.slint -o AppWindow.g.cs \
--csharp-namespace MyApp \
--csharp-runtime-path ui/app-window.slint
--csharp-namespacesets the namespace of the generated classes.--csharp-runtime-pathis where the generated code loads the.slintfile at run time, relative to the application directory. Copy the.slintfiles there, as the build does.- For a project file, pass its directory instead of a file, and the directory as the runtime path.
The other settings map to options:
| Setting | Option |
|---|---|
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.
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 for a virtual keyboard:
app.Window.DispatchEvent(new WindowEvent.KeyPressed("a"));
app.Window.DispatchEvent(new WindowEvent.KeyReleased("a"));
Drawing With OpenGL
Draw below or on top of the scene, or into a texture that an Image shows, with OpenGL.
Require a renderer that uses OpenGL before any other use of Slint, then set a rendering notifier:
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 notifier runs with the window's OpenGL context current, so restore the state that it changes, such as bound buffers.
GraphicsApi is valid during the call only, while the function addresses stay valid with the context.
The examples opengl_underlay
and opengl_texture show both ways:
| A shader below the scene | A cube rendered into a texture |
|---|---|
WGPU and Metal give C# no access to Slint's device, so drawing with them isn't available.
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" });
AddRange, InsertRange, and RemoveRange change several rows with one notification.
For a collection of a view model, such as an ObservableCollection<T>, use an ObservableCollectionModel<T>.
It follows the collection's changes, and the changes of items that implement INotifyPropertyChanged.
Map turns the items into the structs of the .slint file:
using var people = new ObservableCollectionModel<PersonViewModel>(viewModel.People);
app.People = people.Map(p => new Person { Name = p.Name, Age = p.Age });
Change the collection from the Slint thread, and dispose the model to stop following it.
To show your own data, derive from Model<T>,
and call NotifyRowChanged, NotifyRowAdded, NotifyRowRemoved, or NotifyReset when it changes.
Override InsertRow and RemoveRow to let .slint code add and remove rows, as ListModel does.
Each notification is a call into Slint, so for rows that change many times per frame,
store the changes and notify each changed row once per frame, as examples/lols does.
Filter, Sort, Map, and Reverse return views of a model that follow its changes:
var todos = new ListModel<TodoItem>();
app.OpenTodos = todos.Filter(todo => !todo.Done).Sort((a, b) => a.Title.CompareTo(b.Title));
Filter and Sort return a FilterModel and a SortModel.
Their SourceRow maps a row of the view to a row of the source,
and Reset applies a condition that changed outside the model, such as a search text.
View Models
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:
- A property binds to the component's property:
Nametoin-out property <string> name. It binds both ways when the view model's property has a setter, and only from the UI for anoutproperty. - A collection, such as an
ObservableCollection<T>, binds to an array property with the same item type, see Models. - A command, such as
SaveCommandfrom[RelayCommand], binds to the callbacksave, and itsCanExecuteto the boolean propertysave-enabled, if the component has it. For an asynchronous command, itsIsRunningbinds to the boolean propertysave-running, such as to show a spinner. - With validation through
INotifyDataErrorInfo, asObservableValidatordoes, the errors ofNamebind to the string propertyname-error, one per line, andHasErrorsbinds tohas-errorslike any property.
The build warns about a member whose type doesn't fit the component's member of the same name.
For other names and conversions, add bindings to the result of Bind,
or bind without the attribute 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)
.Collection("people", vm => vm.People, p => new Person { Name = p.Name })
.Command("submit", vm => vm.SubmitCommand, enabledProperty: "can-submit")
.Property("busy", vm => vm.SubmitCommand.IsRunning)
.Errors("email-error", nameof(viewModel.Email))
.ToViewModel<int>("selected", (vm, value) => vm.Selected = value);
A view model can also bind to an exported global, such as one global per page of the app.
Pass the global's class to [SlintViewModel], and bind through the window's property of that global:
export global SettingsPage {
in-out property <string> user-name;
callback save();
}
[SlintViewModel(typeof(SettingsPage))]
public partial class SettingsViewModel : ObservableObject { /* UserName, SaveCommand */ }
using var settings = app.SettingsPage.Bind(new SettingsViewModel());
Without the attribute, app.Instance.BindGlobal("SettingsPage", viewModel) binds members one by one.
The view model can raise its changes on any thread; the component updates on the Slint thread. Change a collection on the Slint thread.
examples/mvvm/dotnet binds the view models of MVVM-Samples to a Slint UI,
one global per page: ObservableObject, RelayCommand, AsyncRelayCommand, the messenger, a collection, and a validated form.
UI Written in C# (Preview)
Write the UI in C# instead of a .slint file, with the package Slint.Markup from 1.19.0-preview.13, which references Slint:
dotnet add package yurkinh.slint.net.Markup
A component returns its UI from Render(), with a builder for each element and widget:
using Slint.Markup;
using var app = new Counter();
app.Run();
sealed class Counter : Component
{
private readonly Signal<int> _count = 0;
protected override Element Render() => new VerticalBox
{
new Text(() => $"Count: {_count.Value}").FontSize(24),
new Button("Add", () => _count.Value++),
};
}
dotnet new slint --markup creates such an app.
A value, such as
.FontSize(24), doesn't change. A lambda, such as() => $"Count: {_count.Value}", becomes a property, which the page sets again after a change.After a handler, or a change of a
Signal<T>orSignalList<T>that it read while it rendered or in a lambda, the component callsRender()again; the signals can be in state that components share. The same tree only updates the values; a tree that a C# condition changes compiles another page, so useIfandForEachfor parts that come and go. A Debug build writes a warning when that happens.Component<TState>has aState, whichSetState(s => s.Name = name)changes.Computed<T>is a value computed from signals, such asnew Computed<string>(() => $"{first.Value} {last.Value}"), which computes again after a change of a signal that it read.Changes from other threads render once, on the Slint thread.
A component can show other components, which keep their own state, such as fields of the parent:
new HorizontalBox { _left, _right }. A change of a child renders the window that shows it.Navigationis a stack of pages, such as the pages of a tab of aTabWidget; a page callsNavigation.Push(new DetailPage(id))andNavigation.Pop():private readonly Navigation _tasks = new(new TaskListPage()); protected override Element Render() => new TabWidget { new Tab("Tasks") { _tasks }, new Tab("Settings") { _settings }, }.CurrentIndex(() => _tab.Value, index => _tab.Value = index);Each combination of pages compiles once, and the window after a change of page is a new one at the same place, so bind state that the window shows, such as the current tab, as above.
An
in-outproperty binds both ways, such asnew LineEdit().Text(() => _name.Value, text => _name.Value = text).ForEachrepeats a template for each item, andIfshows its child while a condition holds:new ForEach<Todo>(_todos, row => new HorizontalLayout { new CheckBox(row.Get(t => t.Title)) .Checked(row.Bind(t => t.Done, (todo, done) => _todos[_todos.IndexOf(todo)] = todo with { Done = done })), new Button("Remove").OnClicked(row.Do(todo => _todos.Remove(todo))), }), new If(() => _todos.Count == 0) { new Text("Nothing to do") },row.Getreads a field of the item, also of a record in it;row.Bindalso passes the user's changes back, androw.Dohandles a callback, with the item and, if needed, its index;row.Itemis an item without fields, such as a string.new Expr("parent.width / 2")sets a property to a.slintexpression, and.Property(name, type, value)sets one that a builder lacks..Id("picker")names an element, and a callback also takes.slintstatements, which run without C#, such as for a popup; a callback with several handlers runs them in order:new Button("Pick a date").OnClicked(new Expr("picker.show()")), new DatePickerPopup().Id("picker") .OnAccepted(date => _date.Value = date) // a Slint.Struct with year, month, and day .OnAccepted(new Expr("picker.close()")),Slint doesn't allow a component that inherits
PopupWindow, such asDatePickerPopup, inside anifor afor; a component that's shown under anIfreturns such popups fromPopups(), which the page puts at its root.Clickableis a rectangle with a text that the user clicks, for buttons with their own colors and shapes, which the style'sButtondoesn't have:new Clickable("7", () => Pick(7)).BorderRadius(43).Background(white).TextColor(black).The components of your own
.slintfiles get builders too:<SlintFile Include="ui/card.slint" Markup="true" />The build generates a builder for each component that the file exports, in the project's namespace, and the pages import the file from the output directory, or with
SlintEmbedFilesfrom the assembly.
Binding to a View Model
Instead of a component, a tree can bind to a view model, such as one of CommunityToolkit.Mvvm, in the style of CommunityToolkit.Maui.Markup:
var viewModel = new ContactsViewModel();
using var page = Page.Create(new VerticalBox
{
new LineEdit().Bind(LineEdit.TextProperty, (ContactsViewModel vm) => vm.Name, (vm, name) => vm.Name = name),
new Text().Bind(Text.TextProperty, (ContactsViewModel vm) => vm.Errors(nameof(vm.Name))),
new Button("Add").BindCommand((ContactsViewModel vm) => vm.AddCommand),
new ForEach<Contact>(() => viewModel.Contacts, row => new Text(row.Get(c => c.Name))),
}.BindingContext(viewModel));
page.Run();
BindingContextsets the view model of an element and its descendants.Bindbinds a property of a builder, such asLineEdit.TextProperty, to a value of the view model, or both ways with a setter.BindCommandruns a command on a click, and enables the element while the command can run.Row,Column,RowSpan, andColumnSpanplace children in aGridLayout.- The page updates after
PropertyChanged,ErrorsChanged,CollectionChanged, andCanExecuteChanged, also of the lists ofForEachand their items, from any thread.
examples/contacts/dotnet-markup is a form with validation, a list with search, and an asynchronous command.
Pages compile their .slint code once, and keep the result in Page.CacheDirectory,
so the next start reads it in under a millisecond instead of compiling it again.
Page.Style sets the widget style of the pages.
Hot reload of the C# code renders the components again: a changed value keeps the window, and a changed structure replaces it.
examples/todo/dotnet-markup is the todo example in C#,
and examples/digits/dotnet-markup, examples/tasks/dotnet-markup, and examples/controls-gallery/dotnet-markup
the Digits game, the task app, and part of the controls gallery of Comet, ported.
Builders whose names clash with types that apps import, such as Window, Image, and Path, end in Element, such as WindowElement.
The package works with trimming and Native AOT.
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++);
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.
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.
In the browser, only C# code reloads, see Web.
.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,
and ComponentInstance.CreateLive creates a reloading component without generated code.
Use live reload during development only.
On Android, the app loads its .slint files from the APK, so live reload isn't available there yet:
the build ignores SlintLivePreview with the warning SLINT002, and a changed file needs a rebuild and redeploy.
C# Code
.NET Hot Reload applies C# changes, such as the body of a callback handler, to the running application. Use it in one of these ways:
- Run
dotnet watch, which applies each change when you save. - 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, or on save if the IDE has that turned on.
A handler you passed to an On… method runs the new code from its next call,
because Slint keeps calling the same delegate.
On Android, dotnet watch deploys and starts the app with .NET 10, but doesn't apply C# changes,
for any .NET Android app and not only Slint ones.
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 a restart and without losing the state of the application.
What Needs a Restart
- Renaming or removing a property, callback, or function that your C# code uses.
The reload prints a warning for each member it can't set again,
and the generated classes still have the old members, which throw
SlintExceptionuntil you rebuild. - A C# change that .NET Hot Reload can't apply, such as a new method signature or a new type.
dotnet watchoffers to restart the application. - Changes to the project file, such as turning
SlintLivePreviewon or off.
Translations
Mark strings for translation with @tr() in .slint files.
To bundle gettext .po files into the application, set in your project file:
<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, such as a .NET gettext library,
implement ITranslator and pass it to Translations.SetTranslator.
It translates the components that you compiled without bundled translations.
Set SlintNoDefaultTranslationContext to true if your .po files have no context for strings without one.
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.
On Windows, mark Main with [STAThread].
The windowing backend sets up OLE for drag and drop, which needs a single-threaded apartment,
and without one the application crashes when it creates its first window.
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.
Publishing
dotnet publish -c Release -r <runtime identifier> writes the app, the slint_dotnet library,
and the .slint files, which the app loads at run time.
With SlintEmbedFiles set to true, the assembly embeds the .slint files instead,
with the images and fonts they use and the bundled translations, and the compiler reads them from memory.
It's on by default for a single-file publish, which then writes one executable:
dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
With Native AOT, the output is the executable and the slint_dotnet library.
Bundled translations are the exception to memory: the app writes their .po files to a directory of its temporary files at startup.
A path with a space in it doesn't work for the embedded files.
Android (Preview)
The package also runs on Android 8 (API level 26) and later, on arm64-v8a and x86_64 devices and emulators.
From 1.19.0-preview.10, dotnet new slint-android -o MyApp creates the app in one step.
Then dotnet build -t:Run installs it on the running emulator or the connected device.
To set up the project yourself:
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 --prereleaseIn
MyApp.csproj, changeSupportedOSPlatformVersionto 26, add the runtime identifiers that Slint has libraries for, and add your.slintfile:<PropertyGroup> <SupportedOSPlatformVersion>26</SupportedOSPlatformVersion> <RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers> </PropertyGroup> <ItemGroup> <SlintFile Include="ui/app-window.slint" /> </ItemGroup>The template already sets
SupportedOSPlatformVersionto 24: change that line rather than adding a second one.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 noAppWindowclass, and fails with "The name 'AppWindow' does not exist".Replace
MainActivity.cs: derive the main activity fromSlintActivity, and create the components inSlintMain: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: .NET Android only reads them from your own activity. The template's
Resources/layout/activity_main.xmlis no longer used, and you can delete it.Connect a device with USB debugging, or start an emulator, and run the app:
dotnet build -t:RunTo install the APK of a Debug build with
adb installinstead, set<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>, since a Debug build leaves the assemblies out of the APK otherwise.
SlintMain and every callback run on Slint's thread, not on the Android UI thread.
Slint's objects belong to that thread, so an app has one SlintActivity, and it ends the process when Android destroys it,
such as after the Back button.
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 of them run 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.
The assembly embeds the <SlintFile> and <SlintProject> files, with the images and fonts they use, as on iOS.
With SlintEmbedFiles set to false, the build puts only the .slint files into the APK, and SlintActivity extracts them at startup.
examples/todo/dotnet-android is a complete example,
and examples/lols/dotnet-android runs the LOLs per second test on Android.
iOS (Preview)
Slint runs on iOS 15 and later with Skia on Metal, tested in the iPhone simulator and on an iPhone 13.
From 1.19.0-preview.10, the package has the library for iOS,
and dotnet new slint-ios -o MyApp --app-id com.example.myapp creates the app with the steps below already done.
Install the .NET iOS workload, and create a project for
net10.0-ioswith your.slintfile:<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> </Project>Add an
Info.plistthat 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>Create the components in
Main, and callRun, 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(); } }There's no
AppDelegateand noUIApplication.Main: Slint takes their place.Build and start it in the simulator, or with
-r ios-arm64and a signing identity on a device:dotnet build -t:Run
The build embeds Slint's framework into the app, and the assembly embeds the
.slintfiles with the images and fonts they use, as on Android.SlintLivePreviewisn't available on iOS yet; the build ignores it, with the warningSLINT002.iOS ends apps in the background without notice. Keep what the user was doing when the app leaves the screen, with the UIKit notifications of .NET for iOS, which arrive on the Slint thread:
var file = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "state.json"); using var observer = UIKit.UIApplication.Notifications.ObserveDidEnterBackground((_, _) => File.WriteAllBytes(file, SaveState(app)));Read the file back after
Create, as the todo example does.Native AOT works on devices: set
<PublishAot>true</PublishAot>in the project file, not with-p:, which would also apply it to the Slint libraries, and rundotnet publish -c Release -r ios-arm64. Builds for the simulator keep using Mono.
Web (Preview)
Slint runs in the browser on .NET's WebAssembly runtime, with the same C# API and generated code.
It renders into a <canvas> with FemtoVG on WebGL 2.
From 1.19.0-preview.11, the package has the library for the browser.
dotnet new slint-web -o MyApp creates the app with the steps below already done, and dotnet run serves it.
Install the workload that links Slint into .NET's WebAssembly runtime:
dotnet workload install wasm-tools.Create a project with the WebAssembly SDK and your
.slintfile:<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="..." /> </ItemGroup> </Project>Add
wwwroot/index.htmlwith a canvas with the idcanvas, 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
styleattribute, such asstyle="width: 100vw; height: 100vh", the window follows the page.Await
RunAsyncinstead of callingRun, 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.Quitstops the loop, and fails with the exception of a callback.Runthrows in the browser. On the desktop,RunAsyncruns likeRunand returns a completed task, so code that the platforms share can await it.
Text input works with phones' keyboards and input methods, and copy and paste with the browser's clipboard shortcuts and menu.
The build embeds the .slint files and the images they use in the assembly.
dotnet watch applies C# changes while the page runs, and keeps the page's state.
A .slint change needs a restart: press Ctrl+R in dotnet watch, which rebuilds the assembly with the new file.
Reloading the page isn't enough, and SlintLivePreview doesn't apply to web projects.
Limits:
- One window, which renders into the canvas.
- Slint draws text with the Inter font, which it embeds; it can't use the browser's fonts, so scripts that Inter lacks, such as Japanese, show no glyphs.
- The download:
dotnet.native.wasmis about 7 MB with Brotli, which web servers usually serve compressed. - A path with a space in it doesn't work for the
.slintfiles and images. - To use the software renderer instead of WebGL, set
SLINT_BACKENDbefore .NET starts:await dotnet.withEnvironmentVariable("SLINT_BACKEND", "emscripten-software").run();
The todo and gallery examples run in the browser.
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
- Give the app access to
/dev/driand/dev/input: run it as root, or add its user to thevideoandinputgroups. - Stop the desktop first, if the device has one, since it holds the display.
linuxkmspicks a renderer;linuxkms-software,linuxkms-femtovg, andlinuxkms-skia-openglchoose one.- The device needs libinput and libudev, and for OpenGL, GBM and EGL, which the app loads at run time.
- In 1.19.0-preview.9 and earlier, the OpenGL renderers fail with "Error adding gbm buffer as framebuffer"
on display drivers without modifier support, such as
virtio-gpuin virtual machines. Uselinuxkms-softwarethere, or 1.19.0-preview.10 and later.
See Slint's LinuxKMS guide for the renderers and the display settings.
Without Generated Code
Compiler compiles .slint files at run time,
and ComponentInstance accesses properties, callbacks, and functions by name:
using var compiler = new Compiler();
using var result = compiler.BuildFromPath("app-window.slint");
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();
Building From Source
To build the native library, the compiler, and the packages yourself, such as for a platform or a change that isn't published, follow Building the Packages Locally.
Slint Language Manual
The Slint Language Documentation covers the Slint UI description language in detail.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-android36.0 is compatible. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- yurkinh.slint.net.Compiler (>= 1.19.0-preview.13)
-
net10.0-android36.0
- yurkinh.slint.net.Compiler (>= 1.19.0-preview.13)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on yurkinh.slint.net:
| Package | Downloads |
|---|---|
|
DrawnUi.Slint.Net
DrawnUI canvases in apps built on the community Slint .NET bindings (yurkinh.slint.net), on desktop and mobile. Not made or endorsed by Slint. |
|
|
yurkinh.slint.net.Markup
Community build of the Slint .NET bindings, not published by the Slint project. UI written in C# for Slint: builders of the elements and widgets, components like Comet's, and signals, which compile to .slint code. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.19.0-preview.13 | 0 | 11/10/2026 |
| 1.19.0-preview.12 | 48 | 11/9/2026 |
| 1.19.0-preview.11 | 65 | 11/3/2026 |
| 1.19.0-preview.10 | 96 | 10/29/2026 |
| 1.19.0-preview.9 | 80 | 10/28/2026 |
| 1.19.0-preview.8 | 85 | 10/28/2026 |
| 1.19.0-preview.7 | 78 | 10/27/2026 |
| 1.19.0-preview.6 | 75 | 10/26/2026 |
| 1.19.0-preview.5 | 69 | 10/26/2026 |
| 1.19.0-preview.4 | 67 | 10/25/2026 |
| 1.19.0-preview.3 | 73 | 10/20/2026 |
| 1.19.0-preview.2 | 75 | 10/20/2026 |
| 1.19.0-preview.1 | 75 | 10/20/2026 |