Construction d'une Interface avec Ratatui: Organisation, Zones et Widgets!

Par akhakimov, 22 février, 2025
Quadtree

Bienvenue sur mon quatrième blog! Dans le blog précédent, nous avons créé une application Ratatui vide qui démarre. Cependant, aujourd'hui, nous allons nous intéresser à Ratatui à part entière, c'est-à-dire que nous allons explorer ses outils. Commençons!

Qu'est ce qu'on veut dans notre interface?

Dans l'article précédant, nous avions déterminé les éléments essentiels et principaux de notre interface. Sous « Conception de l'Interface Utilisateur », nous avions décider d'avoir:

  • Trois rouleaux, 1 symbole chacun
  • Montant de la mise
  • Montant totale du joueur

De nouveaux éléments ont été pris en compte:

  • Une table de paiement: Indique au joueur ce qu'il cherche et quel genre de gains il obtiendra
  • Bouton tourner: Un bouton d'apparence simple. Il n'a aucune utilité et est juste cosmétique. Nous allons donner la touche "w" pour jouer.
  • Section contrôles: En bas de l'interface, afficher les actions disponibles et leur clé.
  • Le nom de l'application
Disposition de l'interface

Passons au code

Abordons maintenant la partie intéressante! C'est l'heure des aveux. J'ai menti. J'ai dit dans le dernier blog que nous travaillerions UNIQUEMENT avec afficher_machine(). Non. Nous allons essayer d'aller plus loin!

Squelette

1. Zone principale

Précédemment, nous avions défini affichier_machine() dans le fichiers src/iu/iu_machine/mod.rs de cette manière:

pub fn afficher_machine(frame: &mut Frame, zone_principale: Rect) {
}

Ajoutons ceci à l'intérieur:

let layout_principal = Layout::default()
    .direction(Direction::Vertical)
    .constraints(
        [
            Constraint::Ratio(2, 10),
            Constraint::Ratio(6, 10),
            Constraint::Ratio(1, 10),
            Constraint::Ratio(1, 10),
        ]
	      .as_ref(),
     )
     .split(zone_principale);

Importer Constraint et Direction et Layouten haut du fichier:

use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    Frame,
};

Explication

La manière que Ratatui fonctionne est une sorte de zones imbriquées les unes dans les autres (1). Pensez au quadtree (2): un quadtree divise un espace en parties, encore et encore, progressivement. Qu'est ce que je veux dire par là? Actuellement, la zone principale de notre interface est l'écran complète, qui s'appelle zone_principale, ce qui explique split(zone_principale). On va ajouté des zones à la zone principale. Dans le code au-dessus, on divise notre zone verticallement en 4 parties. Notre:

  • 1ère zone: prend 2/10 de l'écran.
  • 2ème zone: prend 6/10.
  • 3ème zone: prend 1/10.
  • 4ème zone: prend 1/10.

zone_principal

Il existe plusieurs d'autres Constraint. Ratio est un variant de l'Enum Constraint.

Pourquoi Ratio? On utilise Ratio ici parce qu'il permet de définir des proportions relatives entre nos zones. Ça permet à notre page de s'adapter dynamiquement à la taille de l'écran.

On a pu utilisé 100 comme dénominateur au lieu de 10. Plus on veut être précis dans les proportions, plus il faut augmenter le dénominateur (3). Par exemple, si on veut que la 2ème zone soit un peu plus grande, on peut mettre le dénominateur à 20 et la taille de sa zone à 13. Évidemment, il faut recalculer la taille de toutes les autres zones si le dénominateur change. Mais ne vous inquiété pas, si le total ne correspond pas au dénominateur, Ratatui met à l’échelle tous les ratios proportionnellement pour s’adapter à l’espace disponible.

Nos quatre zones se trouvent dans un vecteur appelé layout_principal. Nous pouvons accéder à l'une des zones de la zone principale avec son index et l'utiliser pour créer des zones à l'intérieur de celle-ci!

2. Zone rouleaux

Ajouter ce code dans juste après le code ajouter en haut:

let layout_rouleaux = Layout::default()
    .direction(Direction::Horizontal)
    .constraints(
        [
            Constraint::Ratio(1, 3),
            Constraint::Ratio(1, 3),
            Constraint::Ratio(1, 3),
        ]
        .as_ref(),
    )
    .split(layout_principal[1]);

Explication

On fait la même chose ici. On utilise layout_principal[1], qui est la 2ème zone de la zone principal et on crée des zones horizontalement à l'intérieur. On a 3 zones, 3 rouleaux, qui sont de la même taille (1/3).

3. Zone info

let info_layout = Layout::default()
    .direction(Direction::Horizontal)
    .constraints(
        [
            Constraint::Ratio(2, 7),
            Constraint::Ratio(2, 7),
            Constraint::Ratio(3, 7),
        ]
        .as_ref(),
    )
    .split(layout_principal[2]);

Explication

On utilise layout_principal[2], qui est la 3ème zone et on crée des zones horizontalement à l'intérieur. On a 3 zones.

  • La 1ère zone, info_layout[0], qui va afficher la mise et le total.
  • La 2ème zone, info_layout[1], qui va afficher les possibilités.
  • La 3ème zone, info_layout[2], qui va contenir notre bouton.

Populer les zones

1. Le titre

Premièrement, on va garder notre variable qui va contenir notre titre dans un fichier significatif:

  • Créer le fichier src/iu/constants.rs.
  • Intégrer le fichier à notre projet dans src/iu/constants.rs et ajouter pub mod constants;
  • Nommons la variable TITRE_APPLICATION. Code:
pub const TITRE_APPLICATION: &str = r"
   ___            _                _ _____     _ 
  |_  |          | |              | |_   _|   (_)
    | | __ _  ___| | ___ __   ___ | |_| |_   _ _ 
    | |/ _` |/ __| |/ / '_ \ / _ \| __| | | | | |
/\__/ / (_| | (__|   <| |_) | (_) | |_| | |_| | |
\____/ \__,_|\___|_|\_\ .__/ \___/ \__\_/\__,_|_|
                      | |                        
                      |_|                        
                                                 
                💲Gagnez Gros 💲                
"; 

Vous pouvez créer ou trouver du texte et des images ASCII avec ASCII Art Archive.

Ajouter ce code en sous du code précédant:

let titre_application = Paragraph::new(TITRE_APPLICATION)
    .alignment(Alignment::Center)
    .style(Style::default().fg(Color::Red));
frame.render_widget(titre_application, layout_principal[0]);

et importer TITRE_APPLICATION dans src/iu/iu_machine/mod.rs:

use crate::iu::constants::TITRE_APPLICATION;

Importer également Alignment, Color, Paragraph et Style. Voici les importations de tous les outils Ratatui jusqu'à présent :

use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Style},
    widgets::Paragraph,
    Frame,
};

Explication

Ah ha! Notre premier Widget! Paragraph (4). Ici, on créer la variable titre_application qui prend un Paragraph. On centre notre texte TITRE_APPLICATIONgrâce à Alignment::Center et on lui donne la couleur rouge avec Style. Dans la zone layout_principal[0], nous allons afficher notre titre avec frame.render_widget().

2. La zone information

Créer deux constants TITRE et CONTENUE dans constants.rs:

pub const TITRE: [&str; 3] = ["Montant", "Paiement", "[ESPACE]"];

pub static CONTENUE: [&str; 3] = [
    "Mise: $10\nTotale: $1000",
    "🍒🍒🍒 = $100\n🍋🍋🍋 = $200\n🔔🔔🔔 = $500",
    "TOURNER",
];

Ajouter ce code en sous du code précédant dans src/iu/iu_machine/mod.rs:

for ((index, titre), contenue) in TITRE.iter().enumerate().zip(CONTENUE.iter()) {
    let couleur = match index {
        0 | 1 => Color::Yellow,
        _ => Color::Green,
    };

    frame.render_widget(
        Paragraph::new(contenue.to_string()).block(
            Block::new()
                .borders(Borders::ALL)
                .style(Style::default().fg(couleur))
                .title(titre.to_string())
                .title_alignment(Alignment::Center),
        ),
        info_layout[index],
    );
}

et importer les constants TITRE et CONTENUEdans src/iu/iu_machine/mod.rs:

use crate::iu::constants::{CONTENUE, TITRE, TITRE_APPLICATION};

Importer également Borders et Block, deux nouveaux widgets:

use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Style},
    widgets::{Block, Borders, Paragraph},
    Frame,
};

Explication

Nous faisons exactement la même chose ici que pour le titre, mais avec une bordure et un titre pour une zone. Nous itérons avec la boucle for parce que nous voulons éviter la répétition. Les trois zones de la zone layout_principal[2] auront presque les mêmes éléments. Au lieu de définir frame.render_widget() pour les trois zones, nous allons faire une boucle. Nous savons qu'il a 3 zones, 3 titres (1 chacun) et 3 contenus (1 chacun). Nous allons donc itérer soit le vecteur TITRE soit le vecteur CONTENUE. Nous les itérerons en même temps car nous voulons que leur élément remplisse nos zones. Nous utilisons iter() pour itérer, enumerate() pour obtenir leur index et zip() (5) pour itérer les deux vecteurs en même temps.

Nous voulons également que la zone qui affiche le montant et la zone de paiement soient jaune et que notre bouton soit vert. La variable couleur contiendra un Color basée sur l'index donné.

Pour chaque zone, nous voulons une bordure, une couleur, un titre, un contenu et la centré.

3. La zone contrôles

Créer le constant CONTROLES dans constants.rs:

pub const CONTROLES: &str = "<q> Quitter\n<w>Changer mise\n<e> Changer totale\n<espace> Tourner";

Ajouter ce code en sous du code précédant dans src/iu/iu_machine/mod.rs:

frame.render_widget(Paragraph::new(CONTROLES), layout_principal[3]);

et importer le constants CONTROLES dans src/iu/iu_machine/mod.rs:

use crate::iu::constants::{CONTROLES,CONTENUE, TITRE, TITRE_APPLICATION};

Explication

C'est la zone layout_principal[3] qui va contenir l'affichage des contrôles. On utilise \n pour sauter une ligne

4. Les rouleaux

Passons maintenant à la partie un peu plus lourde. Nous allons introduire le Struct et sa création ainsi que Enum, un type que vous avez déjà vu. Nous allons créer un enum appellé Type et un struct, Symbole. Symbole va comporter le champ:

TYPE_: Nous allons donner le variant d'un symbole. On lui a donné un tiret bas parce que Rust possède le mot clé type qui permet de donner un alias à des types existants. Exemple: Donné l'alias àu8; PetitInteger. Nos différents symboles sont :

  • Citron
  • Cloche
  • Cerise
  • Bière
  • Étoile
  • Banane
  • Diamant

Pour les champs permettant de manipuler la probabilité des symboles seront intégrés dans le futur.

Symbole va comporter les méthodes:

  • données(): Retourne un Tuple, un groupe de valeurs de types différents, (6) qui comporte le dessin et la couleur.
  • dessin(): Retourne le premier élément de la méthode données(), le dessin, représantant le type de symbole.
  • couleur(): Retourne le deuxième élément de la méthode données(), la couleur, associé avec le type de symbole.

On va créer un nouveau dossier appelé symboles (src/symboles) et un fichier mod.rs à l'intérieur (src/symboles/mod.rs). Intégrons le nouveau module en ajoutant mod symboles; dans main.rs.

Premièrement trouvons des dessins ASCII pour représenter un citron et un diamant dans ASCII Art Archive et les ajouter dans un constant. Si vous n'en trouvez pas, vous pouvez toujours en créer un comme je l'ai fait pour notre diamant: Ascii Draw Studio. Dans constants.rs:

pub const CITRON_ART: &str = r"
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣀⣠⣤⣴⣶⡶⢿⣿⣿⣿⠿⠿⠿⠿⠟⠛⢋⣁⣤⡴⠂⣠⡆⠀
⠀⠀⠀⠀⠈⠙⠻⢿⣿⣿⣿⣶⣤⣤⣤⣤⣤⣴⣶⣶⣿⣿⣿⡿⠋⣠⣾⣿⠁⠀
⠀⠀⠀⠀⠀⢀⣴⣤⣄⡉⠛⠻⠿⠿⣿⣿⣿⣿⡿⠿⠟⠋⣁⣤⣾⣿⣿⣿⠀⠀
⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣶⣶⣤⣤⣤⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⡇⠀
⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀
⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀
⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⢸⡟⢸⡟⠀⠀
⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣷⡿⢿⡿⠁⠀⠀
⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⢁⣴⠟⢀⣾⠃⠀⠀⠀
⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⣉⣿⠿⣿⣶⡟⠁⠀⠀⠀⠀
⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⣿⣏⣸⡿⢿⣯⣠⣴⠿⠋⠀⠀⠀⠀⠀⠀
⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⠿⠶⣾⣿⣉⣡⣤⣿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⢸⣿⣿⣿⣿⡿⠿⠿⠿⠶⠾⠛⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠈⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
";

pub const DIAMANT_ART: &str = r"     
      __________________      
     /\····/\····/\····/\     
   /···\··/··\··/··\··/···\   
 /......\/....\/....\/......\ 
 ▌░░░░░░░░░░░░░░░░░░░░░░░░░░▐ 
 ▌······/\····▐▌····/\······▐ 
 \\····/··\···▐▌···/··\····// 
   \\·/····\··▐▌··/····\░//   
     \\·····\·▐▌·/·····//     
       \\····\▐▌/····//       
         \\···▐▌···//         
           \\░▐▌░//           
             \\//             
                              
";

Créons notre enum Type et notre struct Symbole dans src/symboles/mod.rs:

pub enum Type {
    Citron,
    Cloche,
    Cerise,
    Bière,
    Étoile,
    Banane,
    Diamant,
}

pub struct Symbole {
    pub type_: Type,
}

Créons maintenant les trois méthodes toujours dans src/symboles/mod.rs:

impl Symbole {
    pub fn données(&self) -> (&str, Color) {
        match self.type_ {
            Type::Citron => (CITRON_ART, Color::Yellow),
            Type::Diamant => (DIAMANT_ART, Color::Blue),
            _ => ("AAA", Color::Red),
        }
    }

    pub fn dessin(&self) -> &str {
        self.données().0
    }

    pub fn couleur(&self) -> Color {
        self.données().1
    }
}

N'oubliez pas d'importer Color et nos deux constants CITRON_ART et DIAMANT_ART:

use ratatui::style::Color;

use crate::iu::constants::{CITRON_ART, DIAMANT_ART};

Explication

Dans notre méthode données(), on utilise un match. match en Rust, comme un case, compare une valeur et en exécute du code pour les correspondances. Ici on compare, le champ type_ à un type que le symbole pourrait avoir. Si c'est un citron, notre tuple va comprendre le dessin du citron et la couleur jaune. Si c'est un diamant, notre tuple va comprendre le dessin du diamant et la couleur bleu. Pour le restant des autres variantes, on va juste retourner un string slice simple "AAA" et la couleur rouge. Pour une explication rapide et facile, nous montrons juste le citron et le diamant.

Les méthodes dessin() et couleur() retournent un élément du Tuple retourné par données() en y accédant avec l'index.

Implveut tout simplement dire « implémenter pour »


Maintenant finissons ce blog en affichant finalement nos trois rouleaux et leur contenu. Dans src/iu/iu_machine/mod.rs, ajouter ce code (on est toujours dans la méthode affichier_machine):

    let citron = Symbole {
        type_: Type::Citron,
    };

    let diamant = Symbole {
        type_: Type::Diamant,
    };

    let symboles = [&citron, &diamant, &citron];

    for (index, symbole) in symboles.iter().enumerate() {
        frame.render_widget(
            Paragraph::new(symbole.dessin().to_string())
                .alignment(Alignment::Center)
                .block(
                    Block::new()
                        .padding(Padding::uniform(8))
                        .borders(Borders::ALL)
                        .border_type(BorderType::QuadrantInside)
                        .border_style(Style::default().fg(symbole.couleur())),
                ),
            layout_rouleaux[index],
        );
    }

N'oubliez pas d'importer Padding, BorderTypeet nos deux types Symbole et Type:

use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Style},
    widgets::{Block, BorderType, Borders, Padding, Paragraph},
    Frame,
};

use crate::iu::constants::{CONTENUE, CONTROLES, TITRE, TITRE_APPLICATION};
use crate::symboles::{Symbole, Type};

Explication

On crée deux objets. Un pour citron, un pour diamant. On leur donne, un variant d'un type de symbole. On crée un vecteur symbolesqui comporte 3 symboles. Puis, vu qu'on a 3 symboles et 3 rouleaux, on itère symboles et on récupère ses élèments et les index de ses éléments. On récupère le dessin et la couleur en les appelant à partir de l'élèment du vecteur.

Chacun des 3 ASCII, ils seront centrés, horizontalement et verticalement grâce à Padding, avec une bordure de type QuadrantInside, qui met en gras notre bordure, avec une couleur associé avec le symbole. Ainsi, la bordure et l'ASCII partageront la même couleur. Finalement, nous les afficherons dans les trois zones de layout_rouleaux.

Produit d'aujourd'hui

Produit

Conclusion

En conclusion, nous avons créé une interface pour notre application Ratatui en utilisant des zones imbriquées avec des données bidons. Cette structure nous a permis de gérer différents éléments tels que les rouleaux, le titre, le montant, le paiement, ainsi que le bouton.

Code

Pour revenir au code vous pouvez accéder au Projet.

Références

  1. RATATUI, Layout, https://ratatui.rs/concepts/layout/ (Page consultée le 20 février 2025).
  2. WIKIPEDIA, Quadtree, https://fr.wikipedia.org/wiki/Quadtree(Page consultée le 20 février 2025).
  3. RATATUI, Enum Constraint https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html (Page consultée le 20 février 2025).
  4. RATATUI, Paragraph, https://ratatui.rs/examples/widgets/paragraph/ (Page consultée le 20 février 2025).
  5. EDUCBA, Rust zip https://www.educba.com/rust-zip/ (Page consultée le 20 février 2025).
  6. RUST BY EXAMPLE, Tuples https://doc.rust-lang.org/rust-by-example/primitives/tuples.html(Page consultée le 21 février 2025).

Étiquettes

Commentaires