Linux Embedded

Le blog des technologies libres et embarquées

Prendre son envol avec Slint

 

C'est quoi Slint ?

Slint est un outil de création d’IHM implémenté en Rust apparu en mai 2020 (sous le nom de SixtyFPS à l'époque). Slint utilise un langage déclaratif proche du Javascript ou du QML pour décrire les éléments graphiques de l’IHM qu’il compile en code natif. Il s’interface avec le reste du programme avec des APIs Rust, C++ ou Javascript. 

Slint se veut évolutif, léger, intuitif et natif, d’où son nom (Scalable, Lightweight, Intuitive, Native) et se destine aussi bien aux applications de bureau, aux interfaces web, qu’aux systèmes embarqués

Le framework s'intègre dans de nombreux IDE, pas besoin d’un éditeur dédié. Cet article utilisera VSCode et live-preview pour directement visualiser les éléments que l’on développe à l’aide d’un viewer intégré à VSCode. D’autres éditeurs sont également supportés, dont QtCreator, Kate, IntelliJ IDEA, Sublime Text etc…

Cependant on peut noter que Slint étant tout jeune, et bien qu’il commence à apparaître dans des projets industriels, il n’a pas encore la maturité de QML. Le nombre de widgets, de ressources, de développeurs maîtrisant l’outil est mécaniquement plus faible et le recul sur la solution est plus restreint.

Une interface Slint est générée à la compilation, ainsi que tous les bindings et callbacks, ce qui permet plus de robustesse et prédictibilité que des outils utilisant de la Just In Time compilation (compilation de certains éléments au runtime) comme QML.

schema de fonctionnement de slint

 

Un exemple d’application : Flight Planner

Cet article présente une mise en pratique de Slint en proposant une interface minimaliste pour Flight Planner, une application Rust en cours de développement pour gérer la préparation de vol pour les pilotes d’avion léger (devis de masse et centrage, itinéraires de vol, calculs de dérive etc…). Cet exemple n’ira pas dans les détails de cette application mais l’utilisera comme prétexte pour faire une première fenêtre qui affichera certaines informations venant de l’application. Pour les appels à des méthodes non détaillées ici, les types de retours seront explicités afin de pouvoir entrer les données manuellement pour l'exercice.

Mise en place de l’environnement

Extension VSCode

NB : Cet article part du principe que l’environnement de développement Rust sous VSCode est déjà en place et se concentrera sur la mise en place spécifique de Slint

Sur VSCode, l’extension Slint va nous permettre d’avoir de l’autocompletion, de la coloration syntaxique, etc.. mais surtout l'accès au live-preview !

Screen de l'extension Slint pour VSCode

Organisation des fichiers : 

L’exemple sera structuré comme ceci : 

flight_planner/
|-------src/
|       [...] // librairies de flight_planner, sur lesquelles cet article ne s’attardera pas
|       |---- main.rs
|-------ui/
|       |------	assets/
|       |------ icons/
|       |       |----------- airport.png
|       |       |----------- plane.png
|       |-------views/
|       |       |--------- aircraft.slint
|       |       |--------- airport.slint
|       |       |--------- menu.slint	
|       |-------- app-window.slint
|------ build.rs
|------ Cargo.toml

Le dossier "src" contient les sources Rust de l’application. Le dossier "ui" contient les sources de l’interface Slint : la fenêtre principale, les composants et les assets utilisés (images).

Pour utiliser Slint dans notre application Rust, il faut importer "slint" et "slint-build" dans le fichier "Cargo.toml" : 


[dependencies]
slint = "1.9.2"
[build-dependencies]
slint-build = "1.9.0"

Le fichier build.rs est le fichier rust permettant de compiler l’interface graphique. Il appelle slint-build et prend le chemin vers le fichier décrivant la fenêtre principale de l’application (ici app-window.slint)

fn main() {
   slint_build::compile("ui/app-window.slint").expect("Slint build failed");
}

 

Écriture des composants Slint

l’IHM a un menu pour choisir entre les pages "avion" et "aérodrome". Les pages contiennent chacune un menu déroulant pour choisir l’avion ou l'aérodrome, un bouton de retour au menu et une liste d’informations sous forme de texte.

 

Le menu : 

Menu de l'application, avec une icone avion et une icone aerodrome

 

La page "avion" : 

La page avion, avec un appareil choisi et ses informations affichées

 

La page "aérodrome" : 

La page aerodrome avec le terrain de Saint Cyr affiché

 

Description de la fenêtre

Le composant MainWindow est créé dans "ui/app-window.slint". Le composant hérite de Window. C’est la racine de l’arborescence des éléments affichés par Slint. Le mot clé "export" permet à ce composant d'être ensuite importé ailleurs, dans un autre contexte.

export component MainWindow inherits Window {
   title: "Flight Planner";
   width: 800px;
   height: 600px;
   background: @linear-gradient(180deg, #6ac0e6 0%, #ebf8e1 50%, #796045 100%);
}

Ici la fenêtre fait 800x600 pixels (width/height) avec comme titre “Flight Planner” (title). La couleur de fond (background) est un dégradé horizontal allant du bleu au marron en passant par le blanc. Ça rappelle un peu un horizon artificiel d’avion, pour rester dans le thème 🙂. Ce dégradé est décrit via la macro linear-gradient qui prend l’angle du segment a suivre pour le gradient, et une suite de couleurs (hexa) avec le pourcentage du segment auquel cette couleur doit être atteinte.

Pour les couleurs, sous VSCode, l’extension Slint permet d'utiliser une palette de couleurs directement dans l'éditeur (donc pas besoin de chercher le code RGB de la couleur ou pire, d'étaler de l’acrylique sur l’écran, ça ne marche pas très bien...).

palette de couleur de l'extension VSCode

Pour prévisualiser le rendu d’un composant, l’extension Slint pour VSCode permet de lancer Live-Preview en cliquant sur le bouton “Show Preview” au-dessus de la déclaration du composant. Cela permet de voir rapidement le rendu visuel mais aussi d’apporter des modifications ou d’ajouter des éléments graphiquement sans écrire directement le code.

Utiliser le live-preview régulièrement pour se rendre compte des modifications est un bon réflexe pour juger des modifications sans pour autant passer par l'étape compilation.

Live-preview du background

Voilà le rendu de la fenêtre. Celle-ci est la toile sur laquelle seront placés les différents éléments, suivant la page à afficher. La stratégie étant de ne pas changer de fenêtre mais simplement de modifier le contenu de celle-ci.

Menu de sélection

Le menu permet de choisir entre les pages “avion” et “aérodrome”. Il est composé de deux images cliquables, chacune représentant une page. Le composant MainMenu est décrit dans "views/menu.slint".

Pour mettre en page les images, celles-ci sont définies dans un bloc HorizontalLayout. Comme son nom l’indique, ce composant place ses composants enfants les uns à côté des autres horizontalement.

schema du layout du menu et screenshot du menu

Le tout est contenu dans un composant “MainMenu” qui représente l'intégralité du contenu de la page 

export component MainMenu {
   in-out property <int> current-item: 0;
   HorizontalLayout {
       spacing: 60px;
       Image {
           width: 200px;
           height: 200px;
           source: @image-url("../assets/icons/plane.png");
           }
       }
       Image {
           width: 200px;
           height: 200px;
           source: @image-url("../assets/icons/airport.png");
       }
   }
}

 

La propriété "current-item" est un int permettant de gérer l'index de la page à afficher.  Le menu est la page 0, les pages avion et aérodromes ont respectivement les index 1 et 2. La gestion du changement de page sera détaillée un peu plus tard. La syntaxe Slint sur les propriétés permet de définir l'accès à celles-ci. Les options sont : 

  • private : accessible et utilisable uniquement par le composant auquel elle appartient (cas par défaut)
  • in : La propriété est un input. Le composant peut lui donner une valeur par défaut, mais les réécritures sont à la charge des utilisateurs du composant, pas du composant lui-même.
  • out : La propriété peut uniquement être écrite par le composant et est read-only de l'extérieur
  • in-out : La propriété peut être lu et écrite depuis n’importe quel endroit.

 

Dans le cas de ce menu, current-item est in-out car la propriété devrait être modifiée depuis le composant lui-même (lors du clic) et par d'autres composants (les autres pages, lors de l'appui sur le bouton retour).

A cette étape, un live preview permet de visualiser le rendu du composant : 

Live Preview du composant MainMenu

Le code présenté plus haut n’est pas tout à fait complet … Il permet d’afficher le menu, mais pour l’instant ce ne sont que des images sans aucune interaction ! 

Dans chaque image, il faut ajouter une zone cliquable et capturer les clics de souris pour changer l’index de la page à afficher. 

Le composant TouchArea permet de capturer des événements de souris ou via un écran tactile. TouchArea possède une callback “clicked” qui est déclenchée lorsque …. on clique dessus ! (étonnant non ?). C’est cette callback qui doit changer le current-item (l’index de la page).

La syntaxe pour implémenter une callback est la suivante : callback => { code des actions à réaliser }.

Voilà donc ce que ça donne dans nos images : 

export component MainMenu {
   in-out property <int> current-item: 0;
   HorizontalLayout {
       spacing: 60px;
       Image {
           width: 200px;
           height: 200px;
           source: @image-url("../assets/icons/plane.png");
           TouchArea {
               clicked => {
                   current-item =1;
               }
           }
       }
       Image {
           width: 200px;
           height: 200px;
           source: @image-url("../assets/icons/airport.png");
           TouchArea {
               clicked => {
                   current-item = 2;
               }
           }
       }
   }
}

 

Pages avion / aérodrome

Pour les pages "avions" et "aérodrome", le procédé serait rigoureusement identique, les deux pages ayant la même fonction et le même layout, mais représentant simplement des types de données différents. Les différences seront donc uniquement sur les images d’illustration et la structure de données à représenter (et donc le texte à afficher). Cette partie se contentera donc de détailler la page avion.

Une structure Slint permet de représenter les données de l’avion à afficher. Ici, elle contient l’immatriculation de l’appareil, son type et sa puissance moteur.

 

export struct AircraftSpec {
   name: string,
   aircraft_type: string,
   power: int,   
}

 

Pour le composant lui-même, tout d’abord, le layout :

Schema du layout de la page avion avec un screenshot de la page

 

Les éléments sont séparés en deux blocs côte à côte, chaque bloc plaçant ses éléments verticalement. Cela se traduit par un HorizontalLayout contenant deux VerticalLayout : le premier contenant une image d’illustration, une liste déroulante pour sélectionner l’avion (ou l'aérodrome pour la page aérodrome) et un bouton de retour au menu. Le second contenant les informations sur la sélection sous forme de texte.

Cette page est représenté par le composant AircraftView, qui est implémenté dans le fichier "views/aircraft.slint"

Le composant doit contenir : 

  • Les informations de l’avion (AircraftSpec)
  • La liste des avions pour la liste déroulante (ComboBox)
  • Le nom de l’avion actuellement sélectionné
  • L’index partagé de la page courante.
  • Une callback pour remonter l’information de la sélection jusqu'à l'application Rust.
import { Button, ComboBox } from "std-widgets.slint";
export struct AircraftSpec {
   name: string,
   aircraft_type: string,
   power: int,   
}
export component AircraftView {
   in-out property <int> current-item: 0;
   in-out property <[string]> aircraft_list;
   in-out property <string> selected_aircraft;
   in-out property <AircraftSpec> aircraft;
   callback aircraft_changed();
   HorizontalLayout {
       spacing: 60px;
       VerticalLayout {
           Image {
               source: @image-url("../assets/icons/plane.png");
               width: 200px;
               height: 200px;
           }
           // En dessous de l’image, on veut une liste déroulante
		// Et encore en dessous un bouton
       }
       VerticalLayout {
           alignment: center;
           Text {
               text: "Aircraft : " + aircraft.name + "\n" +
                   "Type : " + aircraft.aircraft_type + "\n" +
                   "Power : " + aircraft.power;
               font-size: 15pt;
           }
       }
   }
}

 

Premiers Widgets

Slint dispose d’une bibliothèque de widgets (std-widgets). Elle propose de nombreux composants utilisables pour réaliser une IHM standard (boutons, sliders, etc…) dont le visuel varie en fonction du style choisi.

Pour utiliser un widget, il faut l’importer comme ceci : 

import <widget> from std-widgets;

 

Le widget ComboBox permet d’avoir une liste déroulante de valeurs. Elle possède: 

  • Une propriété “model” qui doit contenir la liste des valeurs possibles. Ici, une liste de string : “in-out property <[string]> aircraft_list”
  • Une propriété “current-value” représentant la valeur actuellement sélectionnée
  • Une callback “selected” qui se déclenche lorsque la valeur choisie change.

 

L’implémentation de cette ComboBox dans notre cas donne ceci : 

ComboBox {
	width: 200px;
	height: 50px;
	model: aircraft_list;
	current-value <=> selected_aircraft;
    selected(current-value) => {
    	aircraft_changed();
    }
}

Lorsque la sélection change, cela déclenche la callback aircraft_changed() au niveau du composant AircraftView.

Le widget Button permet de créer un bouton qui déclenche une callback lorsque l’on clique dessus. L'implémentation de la callback est identique à ce qui est fait pour MainMenu, dans les TouchArea.

Button {
	text: "Retour";
	width: 100px;
	height: 50px;
	clicked => {
		current-item = 0;
	}
}

Le clic sur le bouton modifie le current-item en 0, l’index du menu principal.

Voici donc le fichier "aircraft.slint" complet : 

import { Button, ComboBox } from "std-widgets.slint";
export struct AircraftSpec {
   name: string,
   aircraft_type: string,
   power: int,   
}
export component AircraftView {
   in-out property <int> current-item: 0;
   in-out property <[string]> aircraft_list;
   in-out property <string> selected_aircraft;
   in-out property <AircraftSpec> aircraft;
   callback aircraft_changed();
   HorizontalLayout {
       spacing: 60px;
       VerticalLayout {
           Image {
               source: @image-url("../assets/icons/plane.png");
               width: 200px;
               height: 200px;
           }
           ComboBox {
               width: 200px;
               height: 50px;
               model: root.aircraft_list;
               current-value <=> selected_aircraft;
               selected(current-value) => {
                   root.aircraft_changed();
               }
           }
           Button {
               text: "Retour";
               width: 100px;
               height: 50px;
               clicked => {
                   root.current-item = 0;
               }
           }
       }
       VerticalLayout {
           alignment: center;
           Text {
               text: "Aircraft : " + aircraft.name + "\n" +
                   "Type : " + aircraft.aircraft_type + "\n" +
                   "Power : " + aircraft.power;
               font-size: 15pt;
           }
       }
   }
}

La composition de la page "aérodrome" est rigoureusement identique, si ce n’est la structure de donnée (AircraftSpec devient AirportSpec contenant le nom de l'aérodrome, son code OACI, le nom de la ville, ses coordonnées et son élévation). Vous pouvez écrire ce composant comme exercice.

Mise en relation des composants

Sélection de la page

La MainWindow se charge de sélectionner le composant à afficher en fonction de l’index (current-item). Il faut donc importer tous nos composants (MainMenu, AircraftSpec, AircraftView, AirportSpec et AirportView) dans app-window.slint

Avec une simple condition : "if (condition) : Composant {}" on peut choisir quel composant initialiser et afficher.

Dans "app-window.slint" :

import { AircraftView, AircraftSpec } from "views/aircraft.slint";
import { MainMenu } from "views/menu.slint";
import { AirportView, AirportSpec } from "views/airports.slint";


export component MainWindow inherits Window {


   title: "Flight Planner";
   width: 800px;
   height: 600px;
   background: @linear-gradient(180deg, #6ac0e6 0%, #ebf8e1 50%, #796045 100%);


   out property <int> current-item: 0; // 0: main menu, 1: balance 


   // Ici on va devoir déclarer les propriétés et callbacks a partager avec les composants et avec l’application Rust


   if (root.current-item == 0) : MainMenu {
      // Initialisation du menu
   }
   if(root.current-item == 1) : AircraftView {
      // Initialisation de la page avion
   }
   if (root.current-item == 2) : AirportView {
 	// Initialisation de la page aérodrome
   }
}

Partage des propriétés et callbacks

Nous avons un certain nombre de propriétés et de callbacks qui doivent être partagées entre nos composants, que ce soit pour qu’ils interagissent entre eux (changement de page) ou pour interagir avec le code Rust : 

  • L’index de la page (current-item)
  • La structure représentant l’avion ou l'aérodrome (AircraftSpec / AirportSpec)
  • Les listes de sélection pour les ComboBox
  • Les noms des sélections courantes.

La syntaxe pour synchroniser les propriétés entre différents composants se fait avec l'opérateur <=>. 

Dans le cas de notre application, il faut déclarer les propriétés  que l’on veut partager dans la MainWindow et les synchroniser avec celles des composants enfants lors de leur déclaration.

La MainWindow doit également avoir les callbacks qui doivent déclencher des actions dans le code Rust. Lors de l’initialisation d’un composant enfant, l'implémentation de la callback fait un appel à celle de MainWindow afin que celle-ci puisse remonter jusqu'à l’application.

import { AircraftView, AircraftSpec } from "views/aircraft.slint";
import { MainMenu } from "views/menu.slint";
import { AirportView, AirportSpec } from "views/airports.slint";


export component MainWindow inherits Window {


   title: "Flight Planner";
   width: 800px;
   height: 600px;
   background: @linear-gradient(180deg, #6ac0e6 0%, #ebf8e1 50%, #796045 100%);


   out property <int> current-item: 0; // 0: main menu, 1: aircraft, 2: airport

   in-out property <[string]> aircraft_list;
   in-out property <AircraftSpec> aircraft;
   in-out property <string> selected_aircraft;

   in-out property <[string]> airport_list;
   in-out property <AirportSpec> airport;
   in-out property <string> selected_airport;


   callback aircraft_changed();
   callback airport_changed();


   if (root.current-item == 0) : MainMenu {
       current-item <=> current-item;
   }
   if(root.current-item == 1) : AircraftView {
       current-item <=> current-item;
       aircraft_list <=> aircraft_list;
       selected_aircraft <=> selected_aircraft;
       aircraft <=> aircraft;
       aircraft_changed => {aircraft_changed();}
   }
   if (root.current-item == 2) : AirportView {
       current-item <=> current-item;
       airport_list <=> airport_list;
       selected_airport <=> selected_airport;
       airport <=> airport;
       airport_changed => {root.airport_changed();}
   }
}

Connexion avec l’application Rust

Une interface graphique sert de pont entre une application et ses utilisateurs. Elle doit permettre la communication avec le code métier de l'application, qui dans ce cas est écrit en Rust.


Flight Planner utilise une base de données pour récupérer les informations sur les appareils ou les aérodromes. Cet article ne va pas dans le détail de la récupération de ces informations et utilisera certaines méthodes en mode “boite noire” pour rester focalisé sur la partie IHM mais le format des structures récupérées sera fourni. Pour réutiliser ce code, il est possible de recréer les structures explicitement dans le "main.rs" pour exercice

Première compilation et démarrage de l’IHM

Les seuls rendus de l’IHM obtenus jusqu'à maintenant sont les aperçus obtenus via le Live-Preview. Il est maintenant temps de compiler et démarrer réellement cette interface, avant de la connecter avec les fonctionnalités de Flight Planner.
Dans src/main.rs, il faut importer les modules slint puis créer et lancer une MainWindow  : 

slint::include_modules!();


fn main() {
   let main_window = MainWindow::new().unwrap();
   main_window.run().unwrap();
}

Ce main minimal permet déjà de lancer l’interface ! Avec "cargo run" : 
Screenshot du menu  page avion avec liste deroulante et textes vides

Les passages entre les différentes pages et les boutons retour étant entièrement gérés dans le code Slint, ceux-ci sont déjà fonctionnels en l'état. Par contre les listes déroulantes et les textes ayant besoin de récupérer des informations depuis le code Rust sont vides.
 

Initialisation des propriétés Slint

Comme pour les composants Slint, cet article détaillera l’implémentation de la page avion, le principe étant rigoureusement identique pour la page aérodrome

Pour pouvoir sélectionner un avion, il faut que la liste déroulante de l’interface soit initialisée avec la liste des aéronefs. 
 

Getters et setters

Slint génère automatiquement les getters et setters pour les propriétés des composants. Une propriété "foo" entraîne donc la génération des fonctions Rust "get_foo()" et "set_foo(value)".

Dans la description de l’interface, le contenu des ComboBox (le modèle : aircraft_list) est de type <[string]>. Slint utilise un type ModelRc pour représenter les collections de données. Slint a également sa propre implémentation pour manipuler des strings au runtime : SharedString. Donc une property <[string]> se traduira en Rust par un type ModelRc<SharedString>. 

Flight Planner a une fonction list_entries qui retourne un Vec<String> avec la liste des éléments demandés depuis une base de données (ici la liste des immatriculations des avions). Il faut donc convertir celle-ci avant de pouvoir set la ComboBox.

fn main() {
   let main_window = MainWindow::new().unwrap();
   let list_aircraft = database::list_entries("../../data/airports.db", "aircrafts", "immat").unwrap();
   let aircraft_model = ModelRc::new(VecModel::from(list_aircraft.into_iter().map(SharedString::from).collect::<Vec<_>>()));
   main_window.set_aircraft_list(aircraft_model);
   main_window.run().unwrap();
}

En compilant ce code, la liste déroulante est bien remplie, mais sélectionner un avion ne permet pas d’afficher les informations de celui-ci car on ne fait rien lorsque la callback "selected_aircraft" est déclenchée
page avion avec la liste deroulante remplie

Gestion des callbacks
Nota-Bene sur les structures de données

Flight Planner utilise une structure Aircraft pour représenter les caractéristiques d’un avion. L’IHM précédemment décrite génère également une structure Rust pour AircraftSpec.
La structure Aircraft est la suivante :

pub struct Aircraft {
   pub immatriculation: String,
   pub aircraft_type: String,
   pub horse_power: i32,
}

La structure AircraftSpec de l’interface Slint a son équivalent en code Rust  : 

struct AircraftSpec {
	name: SharedString,
	aircraft_type: SharedString,
	power: int,
}

Une petite fonction de conversion permet de faire le passage entre les deux formats de structure : 

fn aircraft_view_converter(aircraft: &Aircraft) -> AircraftSpec {
   AircraftSpec {
       name: aircraft.immatriculation.clone().into(),
       aircraft_type: aircraft.aircraft_type.clone().into(),
       power: aircraft.horse_power.clone(),
   }
}
Back to callbacks

Pour une callback "foo" dans le code Slint, une fonction "on_foo(closure)" est générée. Le corps de la fonction à appeler est passé en paramètre sous forme de closure, avec le contexte d'exécution de celle-ci. La callback utilise une weak reference vers main_window pour éviter des problèmes d’ownership circulaire (La MainWindow possède le handler de la callback, qui contient elle même une référence à MainWindow). Lorsque la callback "on_aircraft_changed" est déclenchée, elle appelle la lambda passée en paramètre. Il faut donc écrire notre fonction sous la forme d’une lambda en lui donnant le contexte courant : callback(move || {....})
Dans le cas de la callback "aircraft_changed", elle doit récupérer les informations de l’avion sélectionné pour remplir la structure AircraftSpec de l’IHM Slint et en afficher le contenu : 

   let main_window_weak = main_window.as_weak();

   main_window.on_aircraft_changed(move || {
       let main_window = main_window_weak.unwrap();
       let new_aircraft = Aircraft::import(
           main_window.get_selected_aircraft().as_str()
       ).unwrap();
       main_window.set_aircraft(aircraft_view_converter(&new_aircraft));
   });

La fonction Aircraft::import(string) permet de récupérer un élément de type Aircraft à partir de son immatriculation dans une base de données. Elle ne sera pas détaillée ici, elle peut être remplacée par une implémentation manuelle d’un Aircraft.
get_selected_aircraft et set_aircraft sont des getter / setter génerés par Slint.

La gestion de la page "aérodrome" est strictement identique à celle de la page "avion". Ce qui donne donc le "main.rs" complet suivant : 

mod navigation;
use navigation::{aircraft::Aircraft, airport::Airport, database};




slint::include_modules!();


use slint::{ModelRc, SharedString, VecModel};


fn aircraft_view_converter(aircraft: &Aircraft) -> AircraftSpec {
   AircraftSpec {
       name: aircraft.immatriculation.clone().into(),
       aircraft_type: aircraft.aircraft_type.clone().into(),
       power: aircraft.horse_power.clone(),
   }
}


fn airport_view_converter(airport: &Airport) -> AirportSpec {
   AirportSpec {
       name: airport.name.clone().into(),
       icao: airport.oaci_code.clone().into(),
       city: airport.city.clone().into(),
       longitude: airport.longitude as f32,
       latitude: airport.latitude as f32,
       elevation: airport.elevation,
   }
}




fn main() {
   let main_window = MainWindow::new().unwrap();


   //List aircrafts
   let list_aircraft = database::list_entries("../../data/airports.db", "aircrafts", "immat").unwrap();
   let aircraft_model = ModelRc::new(VecModel::from(list_aircraft.into_iter().map(SharedString::from).collect::<Vec<_>>()));
   main_window.set_aircraft_list(aircraft_model);


   let main_window_weak = main_window.as_weak();


   main_window.on_aircraft_changed(move || {
       let main_window = main_window_weak.unwrap();
       let new_aircraft = Aircraft::import(
           main_window.get_selected_aircraft().as_str()
       ).unwrap();
       main_window.set_aircraft(aircraft_view_converter(&new_aircraft));
   });
  


   //List airports
   let airport_list = database::list_entries("../../data/airports.db", "airports", "ident").unwrap();
   let airport_model = ModelRc::new(VecModel::from(airport_list.into_iter().map(SharedString::from).collect::<Vec<_>>()));
   main_window.set_airport_list(airport_model);

   let main_window_weak = main_window.as_weak();
   main_window.on_airport_changed(move || {
       let main_window = main_window_weak.unwrap();
       let new_airport = Airport::from_oaci_code(
           main_window.get_selected_airport().as_str()
       ).unwrap();
       main_window.set_airport(airport_view_converter(&new_airport));
   });
   main_window.run().unwrap();
}

Voilà l’interface simple de Flight Planner terminée et fonctionnelle ! Elle peut être testée en buildant et lancant l’application avec "cargo run" !
animation de la navigation dans l'interface

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.