Recette — Liste scrollable de joueurs
Un écran avec un champ de recherche, une liste cliquable de joueurs et un panneau de détails qui se met à jour au clic. Modèle typique : liste de membres d'un clan, leaderboard, annuaire de la guilde.
Ce que tu vas construire
SearchFieldpour filtrer en temps réelClickableList<Joueur>avec surbrillance au survol- Card de détails mise à jour au clic (pseudo, statut, points)
Modèle de données
// Définis dans ton mod — adapte les champs à ton cas d'usage
public record Joueur(
String pseudo,
boolean enLigne,
int points,
String rang
) {}
Code complet
package ca.monmod.client.screen;
import ca.tawess123.apocalyinterface.api.data.DataSource;
import ca.tawess123.apocalyinterface.api.layout.Card;
import ca.tawess123.apocalyinterface.api.layout.Column;
import ca.tawess123.apocalyinterface.api.layout.Divider;
import ca.tawess123.apocalyinterface.api.layout.Spacer;
import ca.tawess123.apocalyinterface.api.screen.ApocalyScreen;
import ca.tawess123.apocalyinterface.api.theme.ApocalyColors;
import ca.tawess123.apocalyinterface.api.widget.ClickableList;
import ca.tawess123.apocalyinterface.api.widget.KeyValueRow;
import ca.tawess123.apocalyinterface.api.widget.Label;
import ca.tawess123.apocalyinterface.api.widget.SearchField;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import java.util.List;
public final class MembresScreen extends ApocalyScreen {
private final List<Joueur> tousLesMembres;
private String searchQuery = "";
private Joueur selection = null; // null = aucune sélection
public MembresScreen(List<Joueur> membres) {
super(Component.translatable("monmod.screen.membres"), 260);
this.tousLesMembres = membres;
}
@Override
protected void buildLayout(Column root) {
// ── Champ de recherche ──────────────────────────────────────────────
root.add(SearchField.builder()
.placeholderKey("monmod.search.membres")
.onChange(query -> {
searchQuery = query;
update(); // préserve le scroll de la liste
})
.build());
// ── Liste filtrée ───────────────────────────────────────────────────
List<Joueur> filtres = tousLesMembres.stream()
.filter(j -> j.pseudo().toLowerCase().contains(searchQuery.toLowerCase()))
.toList();
var font = Minecraft.getInstance().font;
root.add(ClickableList.<Joueur>builder()
.items(filtres)
.visibleRows(6)
.rowHeight(14)
.rowRenderer((gfx, joueur, x, y, w, mx, my) -> {
// Pastille de statut
int dotColor = joueur.enLigne() ? ApocalyColors.SUCCESS : ApocalyColors.DANGER_SOFT;
gfx.fill(x + 2, y + 3, x + 5, y + 6, dotColor);
// Pseudo
int textColor = joueur.enLigne() ? ApocalyColors.TEXT : ApocalyColors.MUTED_TEXT;
gfx.drawString(font, joueur.pseudo(), x + 8, y + (14 - 9) / 2, textColor, false);
// Points à droite
String pts = joueur.points() + " pts";
int ptX = x + w - font.width(pts) - 3;
gfx.drawString(font, pts, ptX, y + (14 - 9) / 2, ApocalyColors.ACCENT, false);
})
.onSelect(joueur -> {
selection = joueur;
update();
})
.build());
// ── Séparateur ──────────────────────────────────────────────────────
root.add(Spacer.of(4));
root.add(Divider.create());
root.add(Spacer.of(4));
// ── Card de détails ─────────────────────────────────────────────────
if (selection == null) {
root.add(Label.builder()
.text("monmod.label.select_membre")
.muted()
.centered()
.build());
} else {
root.add(buildDetailsCard(selection));
}
}
private Card buildDetailsCard(Joueur j) {
return Card.create()
.add(Label.builder()
.text("monmod.label.membre")
.value(DataSource.of(j.pseudo()))
.title()
.build())
.add(KeyValueRow.builder()
.key("monmod.row.statut")
.value(DataSource.of(j.enLigne() ? "En ligne" : "Hors ligne"))
.valueColor(j.enLigne() ? ApocalyColors.SUCCESS : ApocalyColors.DANGER)
.build())
.add(KeyValueRow.builder()
.key("monmod.row.rang")
.value(DataSource.of(j.rang()))
.build())
.add(KeyValueRow.builder()
.key("monmod.row.points")
.value(DataSource.of(j.points() + " pts"))
.build());
}
}
Ouvrir l'écran
// Depuis le handler du packet S2C contenant la liste des membres
Minecraft.getInstance().execute(() ->
Minecraft.getInstance().setScreen(
new MembresScreen(payload.membres())
)
);
Clés i18n requises
{
"monmod.screen.membres": "Membres du Clan",
"monmod.search.membres": "Rechercher un membre…",
"monmod.label.select_membre": "Sélectionne un membre",
"monmod.label.membre": "Membre : %s",
"monmod.row.statut": "Statut",
"monmod.row.rang": "Rang",
"monmod.row.points": "Points"
}
Pourquoi update() plutôt que init()
update() relance buildLayout sans remettre le scroll à zéro. Quand l'utilisateur filtre la liste et scroll vers le bas, update() filtre sans le téléporter en haut. init() casserait l'expérience.