Bonjour à tous! Bienvenue dans mon sixième article. Notre avant-dernier. Dans le blog précédent, nous avons ajouté l'implémentation à notre application machine, qui permet désormais d'afficher des symboles aléatoires à l'aide de l'algorithme aléatoire pondéré. Aujourd'hui, nous allons afficher des fenêtres contextuelles pour permettre à nos utilisateurs d'interagir davantage avec notre machine à sous et qui va permettre de placer une mise ou d'ajouter de l'argent à leur compte la semaine prochaine. C'est parti!
La démonstration de ce que nous allons accomplir ensemble dans ce blog.
Par où commencer?
Intrinsèquement, une fenêtre contextuelle est une superposition visuelle d'une page actuelle et non une nouvelle page qui, dans notre contexte, nous permettra de recueillir les saisies de l'utilisateur qui souhaite modifier leur montant.
Ratatui ne propose malheureusement pas nativement de widget « Popup » dédié (1). Cependant, nous pouvons créer notre propre grâce au système de mise en page Ratatui et au rendu par blocs pour en simuler un.
En ce qui sont les entrées utilisateurs, comme mentionné dans la documentation officielle, Ratatui nous propose d'utiliser soit le « crate » tui-textarea
ou tui-input
pour gérer les saisies de texte plus facilement. En lisant le code que Ratatui nous propose, cela peut être intimidant (2). L'exemple fourni par Ratatui est complexe, parce qu'il gère manuellement la position du curseur, la saisie de texte et la suppression. Il aborde également des sujets peu familiers pour certains, comme une gestion rigoureuse au niveau des octets.
Comme si nous avions le choix entre les deux « crate », utilisons tui-input
.
Construction de la fenêtre contextuelle
Bon. Commençons par ce que nous avons appris. Pour notre fenêtre contextuelle, nous voulons qu'elle soit centrée sur l'écran et qu'elle soit plus petite que la page principale. Pour ce faire, nous procéderons de la même manière que pour ce qu'on a fait pour afficher_machine()
dans src/iu/iu_machine/mod.rs
. Plutôt que d'utiliser directement l'aire de l'écran principal comme zone principale, nous l'utiliserons pour définir notre propre zone spécifique. Qu'est-ce que je veux dire par la?:
1. Dimensionnement d'un rectangle proportionnellement dans une zone donnée
Ajoutons la méthode centrer_rect()
qui retourne Rect
, notre zone.
pub fn centrer_rect(pourcentage_x: u16, pourcentage_y: u16, r: Rect) -> Rect {
let longueur = r.width * pourcentage_x / 100;
let hauteur = r.height * pourcentage_y / 100;
let marge_droit_gauche = (r.width - longueur) / 2;
let marge_haut_bas = (r.height - hauteur) / 2;
Rect {
x: marge_droit_gauche,
y: marge_haut_bas,
width: longueur,
height: hauteur,
}
}
Explication
Comme vous pouvez le voir, c'est un peu de mathématiques. Ce n'est pas aussi difficile que ça en a l'air. Pour dimensionner notre fenêtre, on va prendre la zone de l'écran (représenté par r
) et la réduire en appliquant une proportionnalité basée sur un pourcentage. Les dimensions (longeur
et hauteur
) sont calculées en multipliant respectivement la largeur et l'hauteur totale de la zone r
.
En Ratatui, le système des coordonnées (3) va de la gauche vers la droite (l'axe des abscisse: x) et de haut vers bas (l'axe des ordonnée: y). Alors la coordonée (0, 0), le point d'origine, est sur le coin en haut à gauche. Tous les Layout
, comme Rect
, définissent leur position par rapport à leur coin supérieur gauche.
En ce qui est la position de notre Rect
:
Pour centrer notre rectangle, le calcul des marges
marge_droit_gauche
et marge_haut_bas
permet de répartir également des deux côtés du rectangle. Ces marges servent ensuite à déterminer les coordonnées x et y. Dans notre exemple:
- Pour la marge droite et gauche = (100 - 40) / 2 = 30 = x
- Pour la marge supérieure et inférieure = (60 - 20) / 2 = 20 = y
2. La fenêtre
pub fn afficher_fenêtre_contextuelle(
frame: &mut Frame,
zone_principal: Rect,
application: &mut Application,
) {
let fenêtre_zone = centrer_rect(20, 7, zone_principal);
let fenêtre_block = Block::default()
.title("un titre".to_string())
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::new().fg(Color::Yellow).bg(Color::DarkGray));
let fenêtre_contenu = Paragraph::new("du texte").block(fenêtre_block);
frame.render_widget(Clear, fenêtre_zone);
frame.render_widget(fenêtre_contenu, fenêtre_zone);
}
Explication
Comme mentionné au-dessus, on va maintenant pouvoir s'en servir de la zone principale pour en créer notre propre zone principale pour notre fenêtre. La fameuse zone est nommée fenêtre_zone
qui est le retour de notre fonction centrer_rect()
. Notre fenêtre fait 20% de la largeur de la zone de l'écran et 7% de l'hauteur.
Le style du bloc qui contiendra les données est défini de cette manière: Un titre centré, une bordure aux coins arrondis, une police jaune et un arrière-plan gris.
On doit définir deux fois render_widget()
. Une fois pour notre paragraphe et une autre pour rendre l'arrière-plan opaque parce que par défaut Block
est transparent.
Gestion des saisies utilisateurs
1. Modes de saisie
Ajoutons le « crate » choisi,tui-input
dans le fichier Cargo.toml
en-sous [dependencies]
:
tui-input = "0.11.1"
Pour gérer la saisie, on va utiliser directement notre Struct Application
dans src/application.rs
et se baser d'un exemple fournit par tui-input
. Donc, ajoutons les champs:
-
pub saisie: Input
: La saisie même de l'utilisateur -
pub saisie_mode: SaisieMode
: Pour gérer l'état de la saisie. On peut quitter une saisie, continuer ou commencer une saisie -
pub affichage_contextuel: bool
: Un booléen pour spécifier si on est actuellement dans la fenêtre
SaisieMode
est notre nouveau Enum. Il définit comme ça:
#[derive(PartialEq)]
pub enum SaisieMode {
Normale,
Édition,
}
Hmm... Un derive? C'est quoi ça? (4) En Rust, le mot-clé derive
permet d'implémenter automatiquement des traits, des fonctionnalités standard souvent utilisées par les « Rustaceans ». Utilisé pour des types personnalisés comme des Struct ou des Enum. Dans notre cas, on utilise #[derive(PartialEq)]
pour génèrer automatiquement l'implémentation du trait PartialEq
(info), permettant de comparer des instances avec les opérateur ==
et !=
. Pour les types sans de « relations d'équivalence ». On va voir l'utilité par la suite.
N'oubliez pas d'importer: use tui_input::Input;
2. Gestion mode de saisie
Comme si on a ajouté de nouveaux champs, il faut mettre à jour la fonction associée initiliser()
:
pub fn initialiser() -> Self {
let mixeur = Mixeur::symboles();
let symboles = mixeur.mélanger(&mixeur.rouleaux);
Self {
mixeur,
symboles,
saisie: Input::default(),
saisie_mode: SaisieMode::Normale,
affichage_contextuel: false,
}
}
Pour changer le mode de saisie et quitter la fenêtre, nous pourrions créer des mutateurs. Créons donc une fonction qui nous place en mode normal et une autre en mode édition. N'oubliez pas que lorsque nous sommes en mode édition, la légende des contrôles disponibles change également. Nous ne pouvons pas ouvrir une fenêtre si nous y sommes déjà, par exemple:
pub fn éditer(&mut self) {
self.saisie_mode = SaisieMode::Édition;
self.affichage_contextuel = true;
}
pub fn arrêter_édition(&mut self) {
self.saisie_mode = SaisieMode::Normale;
self.affichage_contextuel = false;
}
pub fn contrôle_indices(&self) -> &'static str {
match self.saisie_mode {
SaisieMode::Normale => {
"<q> Quitter\n<w> Changer mise\n<e> Changer totale\n<espace> Tourner"
}
SaisieMode::Édition => "<esc> Quitter mode édition\n<enter> Enregistrer",
}
}
pub fn soumettre(&self) {}
soumettre est une méthode vide qu'on implémenterons dans le blog prochain.
Enlevez donc la constante CONTROLES
dans src/iu/constants.rs
et changer ce code dans src/iu/iu_machine/mod.rs
:
frame.render_widget(
Paragraph::new(application.contrôle_indices()), <-- REMPLACEZ CONTROLES
layout_principal[3],
);
3. Fenêtre contextuelle dynamique
Puisque nous disposons d'une seule fenêtre, nous souhaitons l'utiliser lorsque l'utilisateur souhaite modifier sa mise ou augmenter son montant total. Pour ce faire, nous allons créer un autre Enum appelé TypeContextuel
. Pour l'instant, nous allons l'utiliser pour modifier dynamiquement le titre de notre fenêtre. Appuyer sur « w » pour modifier le total, ou sur « e » pour modifier la mise. Revenons à Application
et ajoutons le champ:
pub type_contextuel: TypeContextuel
ainsi que l'Enum:
pub enum TypeContextuel {
Mise,
Totale,
}
Encore une fois, mettons à jour la fonction associée initiliser()
. Ajoutez dans Self
:
type_contextuel: TypeContextuel::Mise
Car pour identifier le type de fenêtre, il faut ouvrir la fenêtre et donc être en mode de saisie Édition. On va donc également mettre à jour la méthode éditer()
:
pub fn éditer(&mut self, type_contextuel: TypeContextuel) {
self.saisie_mode = SaisieMode::Édition;
self.type_contextuel = type_contextuel;
self.affichage_contextuel = true;
}
Gestion des actions clavier
Il faut trouver un moyen de gérer les raccourcis clavier pour interagir avec l'application lorsque nous utilisons différents modes de saisie. Par exemple, quitter l'application lorsque nous ne sommes pas en mode édition ou quitter ce mode pour revenir en mode normal. Dans des cas classiques comme celui-ci, nous sommes obligés d'utiliser des instructions if. Mais nous écrivons en Rust, et c'est facile avec le mot-clé « match ».
Affrontons cette tâche de cette manière. Dans src/controle.rs
l'implémentation/la modifiction de la fonction traiter_événement_clavier()
ressemble à ceci:
pub fn traiter_événement_clavier(application: &mut Application) -> AppResultat<()> {
let event = event::read()?;
if let Event::Key(key) = event {
if key.kind == KeyEventKind::Press {
match application.saisie_mode {
SaisieMode::Normale => match key.code {
event::KeyCode::Char('q') => return Err("erreur".to_string().into()),
event::KeyCode::Char(' ') => application.mélanger_symboles(),
event::KeyCode::Char('e') => application.éditer(TypeContextuel::Totale),
event::KeyCode::Char('w') => application.éditer(TypeContextuel::Mise),
_ => {}
},
SaisieMode::Édition => match key.code {
event::KeyCode::Esc => application.arrêter_édition(),
event::KeyCode::Enter => application.soumettre(),
event::KeyCode::Char(c) if c.is_digit(10) => {
application.saisie.handle_event(&event);
}
event::KeyCode::Backspace => {
application.saisie.handle_event(&event);
}
_ => {}
},
}
}
}
Ok(())
}
Explication
Quand on est mode saisie Normale, on a droit aux touchex:
- « q »: Quitter l'application
- « Espace »: Tourner les rouleaux (changer la combinaisons des symboles)
- « e »: Ouvrir la fenêtre en type Totale
- « w »: Ouvrir la fenêtre en type Mise
Quand on est mode saisie Édition, on a droit aux touchex:
- « ESC »: Quitter le mode saisie édition et passer à Normale
- « Entrée »: Soumettre la saisie
- « Toutes les touches numériques » : Saisir le chiffre
- « Backspace »: Supprimer des caractères
Il est extrêmement utile de mettre en œuvre une fonctionnalité qui permet d'éviter les saisies d'utilisateurs inconnues ou indésirables le plus tôt possible, car les utilisateurs sont malins. event::KeyCode::Char(c) if c.is_digit(10)
est un exemple. L'utilisateur ne peut écrire que des nombres. La méthode handle_event()
(5) est une méthode donnée de saisie
, de type Input, Struct de tui-input
.
Gestion du curseur
Le curseur va appaître dans notre fenêtre. L'implémentation du curseur va se faire donc dans la méthode afficher_fenêtre_contextuelle()
. Ajoutez donc ce code alors à l'intérieur de celle-ci:
let défilement_saisie = application
.saisie
.visual_scroll(fenêtre_zone.width as usize);
if application.saisie_mode == SaisieMode::Édition {
let x = application.saisie.visual_cursor().max(défilement_saisie) - défilement_saisie + 1;
frame.set_cursor_position((fenêtre_zone.x + x as u16, fenêtre_zone.y + 1));
}
Explication
Ce code au complet est tiré d' ici.
Le code ajuste la position du curseur dans notre champ de saisie de texte en tenant compte du défilement. Quand on est en mode Édition, on calcule la position du curseur et met à jour l'endroit où il apparaît sur l'écran en fonction de la position de défilement actuelle.
Gestion du titre dynamique
On arrive vers la fin. Dans la méthode afficher_machine()
, ajoutez:
if application.affichage_contextuel {
match application.type_contextuel {
TypeContextuel::Mise => {
afficher_fenêtre_contextuelle(frame, zone_principal, application, "Changer mise")
}
TypeContextuel::Totale => {
afficher_fenêtre_contextuelle(frame, zone_principal, application, "Changer totale")
}
}
}
Explication
Quand la fenêtre est afficher (n'oubliez pas que affichage_contextuel est un champ booléen), si on est dans le type Mise, le titre est « Changer mise ». Si type Totale, « Changer totale »
Ajouter à votre méthode afficher_fenêtre_contextuelle()
titre: &str
dans la signature. Changer également:
let fenêtre_block = Block::default().title("un titre".to_string())
par
let fenêtre_block = Block::default().title(titre.to_string())
Conclusion
Dans ce blog, nous avons implémenté une fenêtre contextuelle pour notre machine à sous et intégré de nouvelle raccourcis clavier pour notre utiliisateur.
La démonstration de ce que nous avons accompli ensemble dans ce blog.
La semaine prochaine
La semaine prochaine, nous terminerons notre projet en implémentant enfin la fonctionnalité principale, le jeu d'argent. Avec les probabilités maintenant déterminées et les combinaisons de symboles déjà fonctionnelles, ce sera facile.
Code
Pour revenir au code vous pouvez accéder au Projet.
Références
- RATATUI, Popup, https://ratatui.rs/examples/apps/popup/ (Page consultée le 13 mars 2025).
- RATATUI, User input https://ratatui.rs/examples/apps/user_input/ (Page consultée le 13 mars 2025).
- RATATUI, Layout https://ratatui.rs/concepts/layout/ (Page consultée le 14 mars 2025).
- RUST BY EXAMPLE, Derive https://doc.rust-lang.org/rust-by-example/trait/derive.html (Page consultée le 14 mars 2025).
- DOCS.RS, Struct Input https://docs.rs/tui-input/latest/tui_input/struct.Input.html#method.handle_event (Page consultée le 14 mars 2025).
Commentaires