Slint UI é um novo framework para criação de interfaces gráficas. Hoje podemos dizer que o Qt é o maior player no segmento de interfaces gráficas para sistemas embarcados, mas o Slint promete ser uma alternativa mais leve, performática, escrito em Rust e tem sua própria linguagem declarativa para criação de interfaces gráficas (como no caso do QML). Mas esse post não é para explicar o que é o Slint, se você não conhece ainda, recomendo dar uma olhada no site oficial e no repositório do projeto no GitHub, vale a pena.
Slint tem suporte há multiplas linguagens de programação, através de bindings. Como o core é escrito em Rust fica fácil criar bibliotecas para integração com outras linguagens e arquiteturas. O projeto já disponibiliza bindings para C++, JavaScript e lógico Rust. Mas eu como um bom fã de C# não poderia deixar de criar um binding para .NET. E é isso que vou mostrar nesse post.
Foi bem interessante criar esses bindings, um dos desafios envolvidos foi criar de forma dinâmica, as propriedades e callbacks descritos no aquivo .slint
, para serem acessíveis no código C#. E aqui o .NET brilhou com o C# source generators, que permite gerar código em tempo de compilação.
No Program.cs
, mesmo ainda não tendo o código gerado pelo source generator, utilizamos as propriedades descritas no arquivo .slint
, escrevemos toda a lógica e rodamos a janela Slint principal. Durante a compilação, com dotnet build
, o source generator vai primeiro ler o arquivo .slint
e gerar a classe Window
com as propriedades e callbacks descritos. Depois vai compilar o código gerado pelo source generator e o código escrito no Program.cs
, "cuspindo" o executável do programa.
Durante a execução do programa o arquivo .slint
ainda é utilizado, pois o programa só conhece as propriedades e callbacks, mas não sabe como criar a janela. Então o arquivo .slint
é interpretado e a janela é criada de acordo.
Para que o source generator consiga ler o arquivo .slint
e gerar o código, ele precisa ser adicionado no projeto, .csproj
. Para isso no arquivo .csproj
adicione:
<!-- Slint files need to be added to the project -->
<ItemGroup>
<AdditionalFiles Include="./ui/AppWindow.slint">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</AdditionalFiles>
</ItemGroup>
⚠️ Lembre-se de adicionar o arquivo.slint
que tem o componentinherits Window
, os outros imports não são necessários.
Realizar operações de IO dentro de um source generator não é recomendado, e dessa forma o arquivo fica disponível para o source generator dentro do context.AdditionalFiles
.
Como descrito acima primeiro temos que ter o código gerado pelo source generator para depois compilar o código escrito no Program.cs
, que utiliza do código gerado, mas ele ainda não foi gerado. Epa, temos um problema do ovo e da galinha aqui 🥚🐔. A mente humana é hábil o bastante para imaginar e saber como vai ser o resultado final, então podemos fazer essa brincadeira mental e escrever o Program.cs
como deve ser. Mas se você estiver no século 21 e utilizar uma IDE ou editor de código com analisador para te ajudar, ele vai reclamar que as propriedades, callbacks e a classe Window
não existem 🤪.
Bom, eu uso VS Code, e no VS Code com a nova extensão do C# foi exatamente isso que aconteceu. Parece ser uma issue, ou não é suportado ainda 🤔 (esse nova extensão do C# com o C# dev kit é uma coisa meio polemica 🙄). Maaas, há uma solução, voltar para o bom e velho OmniSharp. No settings.json
, global ou do seu workspace, use:
"omnisharp.useModernNet": true,
"dotnet.server.useOmnisharp": true,
Assim continuamos usando a última versão do .NET instalada ao invés do mono
mas com o OmniSharp. O OmniSharp consegue analisar o assembly gerado do resultado final do source generator, e assim consegue nos fornecer o autocomplete e dicas que precisamos.
Se você quiser testar em um template já configurado com o getting started, você pode utilizar o template em test do SlintDotnet para Torizon, e inclusive rodar ele em uma Raspberry Pi com Torizon OS.
Voce vai precisar Instalar o VS Code Torizon IDE Extension e dependências. E então no settings globais do VS Code adicionar:
"apollox.templatesBranch": "castello/labs",
"apollox.templatesTag": "next",
Isso ira carregar os meus templates em "labs", experimentais. E então você pode criar um novo projeto com o template .NET Slint Application
:
Se você não quiser executar o projeto em um Linux embarcado rodando Torizon OS, sem problemas, você pode rodar local. Selecione a opção de debug .NET Slint Local
:
Como minha área de atuação é na verdade a de sistemas embarcados, eu não poderia deixar de testar o SlintDotnet em um dispositivo Linux embarcado. Então desenvolvi um pequeno "Hello World", que no caso de sistemas embarcados tem que envolver piscar LEDs 😆.
O código da demo está disponível no meu GitHub microhobby/slintGpio.
Estou usando as bibliotecas do dotnet/iot para acessar o GPIO do lado do .NET e compartilhando o estado do GPIO com o Slint UI através de uma in-out property
. O Slint fica monitorando a propriedade e atualiza uma representação de um LED na tela. O estado do LED pode ser modificado por um botão, que chama uma callback que está implementada no lado do .NET, ou por um botão físico conectado ao GPIO.
Trabalhar nesses bindings foi bem interessante. Enfim consegui aprender algo de Rust, e da melhor forma na minha visão, com mão na massa. Foi também a primeira vez que trabalhei com o C# source generators, em um cenário real, é uma feature muito poderosa.
O Slint é um projeto bem interessante, e acredito que tem um grande potencial. A linguagem Slint é muito versátil e gostosa de trabalhar, você consegue resolver muita lógica de UI com a própria UI. O contexto de lógica de UI e negócios fica muito bem separada.
O casamento entre Slint e C# ficou muito legal. Me pareceu uma forma realmente moderna de trabalhar com C# top-level statements + GUI. Mas eu sou suspeito em falar, agora é a hora da comunidade usar e me deixar saber o que achou, se vai ser útil ou se vai ser mais um dos meus side-projects jogado para as teias de aranha... 😅