Slint UI is a new framework for creating graphical user interfaces. Today we can say that Qt is the biggest player in the segment of graphical user interfaces for embedded systems, but Slint promises to be a lighter, more performant alternative, written in Rust and has its own declarative language for creating graphical user interfaces (as in the case of QML).But this post is not to explain what Slint is, if you don't know it yet, I recommend taking a look at the official website and the project repository on GitHub, it's worth it.
Slint supports multiple programming languages, through bindings. As the core is written in Rust, it is easy to create libraries for integration with other languages and architectures. The project already provides bindings for C++, JavaScript and Rust. But as a good fan of C#, I couldn't help myself but create a binding for .NET, and that's what I'm going to show in this post.
It was very interesting to create these bindings, one of the challenges involved was dynamically creating the properties and callbacks described in the .slint
file, to be accessible in the C# code. And here .NET shined with C# source generators, which allows you to generate code at compile time.
In Program.cs
, even though we don't yet have the code generated by the source generator, we use the properties described in the .slint
file, write all the logic and run the main Slint window. During compilation, with dotnet build
, the source generator will first read the .slint
file and generate the Window
class with the described properties and callbacks. It will then compile the code generated by the source generator and the code written in Program.cs
, "spitting out" the program executable.
During the execution of the program, the .slint
file is still used, as the program only knows the properties and callbacks, but does not know how to create the window. Then the .slint
file is interpreted again and the window is created accordingly.
For the source generator to be able to read the .slint
file and generate the code, it needs to be added to the project, .csproj
. To do this in the .csproj
file add:
<!-- Slint files need to be added to the project -->
<ItemGroup>
<AdditionalFiles Include="./ui/AppWindow.slint">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</AdditionalFiles>
</ItemGroup>
⚠️ Remember to add only the.slint
file that has theinherits Window
component, the other imports are not necessary.
Performing IO operations within a source generator is not recommended, and this way the file is available to the source generator within context.AdditionalFiles
.
As described above, we first have to have the code generated by the source generator and then compile the code written in Program.cs
, which uses the generated code, but it has not yet been generated. Whoops, we have a chicken and egg problem here 🥚🐔. The human mind is skilled enough to imagine and know what the end result will be like, so we can play this mental game and write Program.cs
as it should be. But if you're in the 21st century and use an IDE or code editor with an analyzer to help you, it will complain that the properties, callbacks and the Window
class don't exist 🤪.
Well, I use VS Code, and in VS Code with the new C# extension that's exactly what happened. It seems to be an issue, or it's not supported yet 🤔 (this new C# extension with the C# dev kit is a bit of a controversial thing 🙄). Buuut, there is a solution, go back to the good old OmniSharp. In settings.json
, global or in your workspace, use:
"omnisharp.useModernNet": true,
"dotnet.server.useOmnisharp": true,
So, we continue using the latest version of .NET installed instead of mono
but with OmniSharp. OmniSharp can analyze the assembly generated from the final result of the source generator, and thus can provide us with the autocomplete and tips we need.
If you want to test on a template already configured with a getting started, you can use the SlintDotnet test template for Torizon, and even run it on a Raspberry Pi with Torizon OS.
You will need to install VS Code Torizon IDE Extension and dependencies. And then in VS Code global settings add:
"apollox.templatesBranch": "castello/labs",
"apollox.templatesTag": "next",
This will load my templates from "labs", experimental. And then you can create a new project with the .NET Slint Application
template:
If you don't want to run the project on an embedded Linux running Torizon OS, no problem, you can run it locally. Select the .NET Slint Local
debug option:
As my field of expertise is actually embedded systems, I couldn't help but test SlintDotnet on an embedded Linux device. So, I developed a little "Hello World", which in the case of embedded systems has to involve blinking LEDs 😆.
The demo code is available on my GitHub microhobby/slintGpio.
I'm using the dotnet/iot libraries to access the GPIO on the .NET side and sharing the GPIO state with Slint UI through an in-out property
. Slint monitors the property and updates a representation of an LED on the screen. The state of the LED can be modified by an UI button, which calls a callback that is implemented on the .NET side, or by a physical button connected to the GPIO.
Working on these bindings was very interesting. Finally, I managed to learn something about Rust, and in the best way in my opinion, with hands-on. It was also the first time I worked with C# source generators, in a real scenario, it is a very powerful feature.
Slint is a very interesting project, and I believe it has great potential. The Slint language is very versatile and fun to work with, you can solve a lot of UI logic with the UI itself. The context of UI and business logic is very well separated.
The "marriage" between Slint and C# turned out really cool. It seemed like a really modern way of working with C# top-level statements plus GUI. But, I'm suspicious to talk, now is the time for the community to use it and let me know what they think, if it will be useful or if it will be another one of my side-projects thrown into the cobwebs... 😅