Sur cette page

← Toute la documentation

Thèmes d'interface

Thèmes d'interface : le contrat des jetons, les choix de mise en page, écrire un thème.

Une interface (Classique, Slant, Cabinet…) est un thème : un dossier simple, lu sur disque comme les mods et les skins, qu'on supprime pour le retirer. Le joueur choisit son thème dans Skins → Interface (réglage uiTheme, l'identifiant du thème).

Pourquoi des données et pas du code

Les scripts n'ont jamais accès au DOM, au CSS ni au JavaScript de la WebView, et toute valeur qui entre dans la page est validée en Rust (AGENTS.md). Un thème est donc de la donnée : un manifeste, un fichier de jetons (un sous-ensemble strict de CSS) et des images. Le crate themes (crates/themes) valide tout contre un contrat de jetons typés, puis l'hôte publie à la page l'événement interfaces avec les seules valeurs qui ont passé. La page les pose comme variables CSS sous [data-direction="<id>"]. Le CSS structurel (plaques, boutons, onglets, liste, graphes) reste dans l'application et ne lit que ces jetons : un thème tiers restyle toute l'interface sans balisage ni script, et ne peut rien exécuter ni rien charger.

Où sont les thèmes

Ordre de recherche : themes/ à côté de prism.exe (en développement : le dossier themes/ du dépôt, trouvé en remontant jusqu'à Cargo.lock), puis <data>/themes (PRISM_THEMES_DIR change ce dossier). Le même identifiant deux fois : le premier gagne, le second est signalé. Chaque dossier s'appelle comme son identifiant (minuscules, chiffres, tirets, 32 caractères au plus).

themes/<id>/
  theme.json     le manifeste
  tokens.css     les jetons
  preview.png    (facultatif) aperçu ; sans lui, la carte du sélecteur est dessinée avec les jetons
  images/…       (facultatif) images citées par les jetons de type image

theme.json

Champ Rôle
id doit être le nom du dossier
name 1 à 48 caractères
description jusqu'à 240 caractères
version lettres, chiffres, . _ + -, 32 au plus
author jusqu'à 64 caractères
order place dans le sélecteur (0 à 1000, 100 par défaut), puis l'identifiant
preview image du dossier (facultatif)
accent "preference" (défaut) : l'accent suit le réglage du joueur (Réglages → Interface → Couleur d'accent : default, c'est le --pv-accent du thème ; artwork, la teinte de l'illustration de la map, optionnelle ; custom, une couleur choisie) ; "fixed" : l'accent est toujours le --pv-accent du thème (couleur signature : vert phosphore…). L'ancien mot "artwork" est accepté et vaut "preference"
tokens fichier de jetons (tokens.css par défaut)
localized { "fr": { "name": "…", "description": "…" }, "zh": { … } } : nom et description par langue (code de langue du catalogue : en, fr, zh)

tokens.css

Un bloc :root { --pv-x: valeur; … } et jusqu'à deux blocs @media (max-width: <n>px) { :root { … } } pour les fenêtres étroites (320 à 4000 px). Rien d'autre : aucun autre sélecteur, propriété, @import, @font-face, imbrication, url(), expression, var() hors couleurs. Les commentaires /* … */ sont permis. Limites : 64 Kio par fichier, 200 déclarations, 600 caractères par valeur, images de 4 Mio au plus. Un jeton absent garde la valeur neutre de l'application : un thème peut être partiel. Un fichier refusé retire son thème de la liste, avec un message précis (numéro de ligne, jeton, raison) dans le journal.

Le contrat des jetons

Types de valeurs :

  • color : couleur : #rgb, #rrggbb, #rrggbbaa, rgb() / rgba() / hsl() / hsla() (nombres, syntaxe virgules ou espaces avec / alpha), color-mix(in srgb, <couleur> [n%], <couleur> [n%]), transparent, white, black, et var(--pv-<jeton couleur>) (la seule référence permise, vers un autre jeton de type couleur, par exemple l'accent).
  • length : longueur : nombre + px, em, rem, %, vw, vh, vmin, vmax (ou 0), clamp(a, b, c), min(a, b), max(a, b).
  • font : pile de polices : 1 à 8 familles (noms entre guillemets ou mots simples : lettres, chiffres, espaces, ., _, -), la dernière une famille générique (sans-serif, serif, monospace, system-ui, ui-monospace, cursive) ; PrismLegacy est la police du jeu.
  • weight : graisse : entier de 1 à 1000.
  • opacity : nombre de 0 à 1.
  • easing : ease, ease-in, ease-out, ease-in-out, linear ou cubic-bezier(x1, y1, x2, y2) (x de 0 à 1, y de -4 à 4).
  • clip : forme : none, polygon(x y, …) (3 à 16 points, coordonnées en longueurs ou calc(<n>% ± <n>px)) ou inset(1 à 4 décalages [round 1 à 4 rayons]).
  • layers : dégradés : none ou 1 à 4 linear-gradient(…) / repeating-linear-gradient(…) séparés par des virgules (angle 90deg ou to right, 2 à 12 arrêts : couleur + 0 à 2 positions).
  • filter : filtre : none ou jusqu'à 6 fonctions parmi saturate, brightness, contrast, grayscale, sepia, opacity, hue-rotate(<n>deg), blur(≤ 12px).
  • image : image : none ou image("chemin/relatif.png") (un fichier image du dossier du thème : png, jpg, webp, gif, svg).
  • case : none, uppercase, lowercase ou capitalize.
  • scheme : dark ou light.
  • choice : un mot pris dans la liste du jeton (voir « Choix de mise en page »). Tout autre mot, plusieurs mots, une valeur vide, un nombre ou un var() sont refusés.
Jetons Type Rôle
--pv-bg color Page background and the colour gaps and cut-outs show.
--pv-plate, --pv-plate-raised, --pv-plate-hover, --pv-plate-active, --pv-plate-active-hover color Surfaces of panels (plate), raised controls, hovered and selected ones.
--pv-line, --pv-track color Hairlines and the empty part of gauges.
--pv-text, --pv-text-2, --pv-text-3 color Primary, secondary and tertiary text.
--pv-ink color Text drawn on an accent-coloured surface.
--pv-accent color The theme's own accent: what the interface uses unless the player opted in to the map's artwork or a colour of their own (accentSource), and always for a theme with "accent": "fixed".
--pv-accent-2 color A second accent for secondary marks (the goal pin, a goal that follows the current judgement).
--pv-ok, --pv-warn, --pv-danger color Success, warning and error tones.
--pv-offset-ok, --pv-offset-warn, --pv-offset-high, --pv-offset-bad color Couleur du décalage moyen sur l'écran de résultat, selon son éloignement de zéro : correct (≤ 2 ms), à surveiller (≈ 4 ms), à corriger (≈ 7 ms), à corriger tout de suite (≥ 10 ms). L'interface interpole en continu entre ces quatre couleurs ; un thème clair les redéfinit pour rester lisible.
--pv-tier-platinum, --pv-tier-diamond, --pv-tier-prism, --pv-tier-prism-b, --pv-tier-prism-c color Couleurs des paliers de succès au-dessus de l'or (platine, diamant, puis prisme, le plus haut ; bronze, argent et or sont les couleurs du podium). Le prisme est un bleu, un violet et un cyan : seul l'anneau de la médaille les fond en dégradé conique, le texte et la barre n'utilisent que --pv-tier-prism. Un thème clair les redéfinit pour rester lisibles.
--pv-gold, --pv-silver, --pv-bronze color Podium ranks.
--pv-graph-surface, --pv-graph-grid, --pv-graph-zero, --pv-graph-cursor color Chart plot background, grid lines, zero line and inspection cursor.
--pv-graph-low, --pv-graph-mid, --pv-graph-high, --pv-graph-peak color Palette des graphes, des valeurs calmes aux plus chargées (la chronologie de densité monte de low à peak). Les couleurs des paliers de jugement et des difficultés ne sont jamais des jetons.
--pv-graph-bar-stroke color Contour des barres de graphe.
--pv-clip-lg, --pv-clip-md, --pv-clip-sm clip Shape of large (rows, panels), medium (buttons, tabs) and small (badges) plates.
--pv-clip-edge-lg, --pv-clip-edge-md, --pv-clip-edge-sm clip Shape of the accent edge drawn on the leading side of each plate size.
--pv-clip-meter, --pv-graph-bar-clip clip Shape of gauge fills and of chart bars.
--pv-art-clip clip Shape of cover pictures.
--pv-inset-lg, --pv-inset-md, --pv-inset-sm length Horizontal padding that clears the slanted or chamfered ends of each plate size.
--pv-edge-w, --pv-meter-h, --pv-radius-input, --pv-radius-panel, --pv-graph-line-w, --pv-graph-bar-gap length (au plus 64px) Accent edge width, gauge height, input corner radius, chart line width and gap between chart bars.
--pv-h-sm, --pv-h-md, --pv-h-lg length Control heights.
--pv-art-inset, --pv-art-left, --pv-art-w, --pv-art-pad length Cover picture of a song row: inset, left offset, width and the text padding it needs.
--pv-set-row, --pv-diff-row length (au moins 8px, au plus 400px) Hauteur d'une carte de set et d'une ligne de difficulté de la liste (rapportée au modèle de la liste).
--pv-set-gap, --pv-diff-gap length (0 à 100px) Espace sous une carte de set et sous une ligne de difficulté.
--pv-shift-base length Distance entrance animations travel (zero under reduced motion).
--pv-rail-w, --pv-drawer-w length (au moins 40px, au plus 600px) Width of the collapsed (icon rail) and expanded Escape drawer.
--pv-display-ls, --pv-label-ls, --pv-title-ls, --pv-hero-ls length (peut être négatif) Letter spacing of display text, labels, titles and big numerals.
--pv-fs-xs, --pv-fs-sm, --pv-fs-md, --pv-fs-lg, --pv-fs-xl, --pv-fs-2xl, --pv-fs-3xl, --pv-fs-hero, --pv-fs-meta, --pv-fs-accuracy, --pv-fs-title, --pv-fs-rating length (au moins 12px) Type scale; every size is at least 12px.
--pv-font-display, --pv-font-body, --pv-font-num, --pv-font-label, --pv-font-mono font Font stacks: display text, body, numerals, labels, technical details (PrismLegacy is the game font).
--pv-fw-display, --pv-fw-num, --pv-fw-label weight Font weights.
--pv-art-opacity opacity Opacity of cover pictures.
--pv-ease, --pv-ease-spring easing Easing of motion and of its springy variant.
--pv-display-case, --pv-label-case case Text transform of display text and labels.
--pv-color-scheme scheme Whether native controls (scrollbars, inputs) are dark or light.
--pv-meter-ticks, --pv-art-mask, --pv-graph-scan, --pv-shade layers Gradient layers: gauge segment ticks, cover fade mask, the chart scanline overlay and the shade over the map picture.
--pv-art-filter filter Filter applied to the map picture behind the menus.
--pv-plate-filter filter Profondeur sous chaque plaque : drop-shadow(<x> <y> [<flou>] <couleur>) (décalages ≤ 48 px), ou none. Un filtre et non une ombre de boîte, car les plaques sont des formes découpées.
--pv-plate-sheen layers Jusqu'à quatre dégradés linéaires peints au-dessus de la couleur de chaque plaque (biseau, relief d'une touche, reflet) ; none pour une plaque plate.
--pv-overlay-image image Une image du dossier du thème dessinée sur l'illustration de la map et sous les menus (vignette, grille, décor), étirée à la fenêtre (le preserveAspectRatio du SVG choisit entre rognage et déformation) : image("images/overlay.svg").
--pv-thumb color Pouce du segment choisi d'un contrôle segmenté (avec --pv-layout-controls: pill) ; la plaque survolée si absent.
--pv-switch color Couleur d'un interrupteur allumé (avec pill) ; l'accent si absent.
--pv-bg-image image Picture of the theme's folder drawn on the page background (image("images/bg.png")).

Choix de mise en page

Un thème peut changer la construction des menus, toujours comme des données : un jeton de type choice prend un mot dans une liste fermée (vérifiée par Rust, puis par la page). La page pose chaque choix valide en attribut data-layout-<nom> sur <html> ; les feuilles de style de l'application (theme/layout.css et les composants) y réagissent avec grid-template-areas, order et des variables. Un thème ne fournit aucun sélecteur ni aucun CSS : seulement ces mots. Le premier mot de chaque liste est la mise en page actuelle : un choix omis (ou refusé) vaut ce mot, donc un thème qui n'en dit rien ne change rien.

Jeton Mots Effet
--pv-layout-list right · left · center Côté de la liste de morceaux ; le panneau de la map est de l'autre côté. center : liste au milieu, panneau à droite, illustration visible à gauche. La hauteur des lignes reste celle des jetons --pv-set-row et --pv-diff-row.
--pv-layout-tabs top · rail · dock · sidebar Onglets de la map : barre en haut, rail vertical à gauche du panneau, dock en bas, ou barre latérale pleine hauteur à gauche de tout le panneau (une liste de source ; elle se replie en icônes dans les fenêtres étroites).
--pv-layout-play bottom · top Notation, jugement et bouton Jouer en bas du panneau, ou en haut.
--pv-layout-density comfortable · compact · dense Espacements et hauteurs de lignes plus serrés d'un cran à chaque palier (la liste virtualisée relit ses hauteurs).
--pv-layout-chrome none · titlebar · nav Bandeau décoratif : titlebar, une barre de fenêtre (trois pastilles et le nom de l'application) ; nav, une fine barre de navigation de site, teinte translucide à 80 % (le seul flou) avec le nom de l'application et le titre de la page. Sans fonction : les vrais boutons sont ceux du système.
--pv-layout-drawer left · right · bottom Bord d'où vient le tiroir d'Échap ; bottom : un dock d'icônes.
--pv-layout-rows cover · strip · text · list Ligne de la liste : vignette à gauche, illustration en bandeau estompé derrière la ligne, texte seul (ligne de tableau), ou ligne plate de list (vignette et texte sans plaque, une teinte sous la ligne survolée et la ligne choisie, comme une liste d'application musicale).
--pv-layout-profile top · rail Identité du joueur sur la page Profil : bandeau en haut, ou rail à gauche.
--pv-layout-cards rich · compact Carte de score du classement : grande précision et compteurs dessous, ou une ligne de tableau.
--pv-layout-buttons plate · bracket · link link : tous les boutons sauf le principal deviennent du texte d'accent sans plaque. Boutons, onglets et boutons-icônes : plaque pleine à la forme du thème, ou contour en crochets dessiné au dégradé (sans remplissage, comme un [ ] de terminal) ; les éléments à fond d'accent gardent l'accent pour les crochets et leur texte.
--pv-layout-focus ring · blink Focus clavier : le contour habituel, ou ce contour qui clignote comme un curseur de texte (fixe, en anneau, sous prefers-reduced-motion).
--pv-layout-icons none · icon Les onglets (Info, Classement, Mods, Entraînement, Éditeur) montrent leur icône avant le libellé.
--pv-layout-groups plain · inset Lignes des Réglages : filets entre sections, ou groupes en cartes arrondies à filets fins entre les lignes (fenêtre de réglages système).
--pv-layout-controls plain · pill Interrupteurs, contrôles segmentés, menus déroulants et barres de défilement : formes du thème, ou interrupteurs en pilule à pastille blanche, contrôle segmenté à segment choisi en relief, menus arrondis à sélection en couleur d'accent, barres fines superposées.
--pv-layout-hero plate · open En-tête de la map choisie et ses quatre chiffres : sur des plaques, ou open, posés sur la page (titre et grand chiffre sans carte, chiffres en colonnes entre filets, comme une page produit).
--pv-rating-dark opacity Même idée pour le texte des valeurs de difficulté et de compétences (la couleur ratingColor elle-même n'est jamais modifiée).
--pv-tier-dark opacity Part de noir mélangée au TEXTE des paliers de jugement (0 : couleurs du jeu). Les thèmes clairs la montent pour que les noms de paliers restent lisibles ; les pastilles et les données ne changent pas. ratingColor n'est jamais touché.

Graphe de progression du replay

Le graphe de progression (combo, précision, densité de la map sur un axe de temps commun) se règle par les mêmes jetons. Le composant lit ces mots (attributs data-graph-* sur <html>) sans aucun balisage par thème ; zoom, panoramique, popovers, légende et masquage des intervalles de miss ne changent pas.

Jeton Valeurs Effet
--pv-graph-progression steps · lines · area · bars steps (défaut) : chaque échantillon tenu jusqu'au suivant, densité en aire pâle ; lines : segments droits entre les vrais échantillons, rien de lissé ; area : courbes en escalier remplies ; bars : densité en barres, combo et précision en escalier. Le joueur peut le remplacer dans Réglages → Interface (« Style du graphe de progression »).
--pv-graph-grid-style lines · none · dots · scanlines Quadrillage derrière les graphes du replay.
--pv-graph-cursor-shape dashed · solid · band Curseur : trait pointillé, plein ou bande translucide.
--pv-graph-marker none · dot · diamond · square Repère là où le curseur croise chaque courbe visible.
--pv-graph-miss-style bands · ticks Intervalles de miss : bandes pleine hauteur ou petites marques en bas.
--pv-graph-curve-w length 1–6 px Épaisseur de la courbe de combo (précision : 75 %, contour de densité : 60 %).
--pv-graph-fill-opacity opacity Opacité des remplissages sous les courbes.
--pv-graph-combo, --pv-graph-accuracy, --pv-graph-density-low, --pv-graph-density-mid, --pv-graph-density-high #rrggbb Couleurs des trois courbes (la densité est un dégradé low → mid → high selon l'ampleur).

Les trois courbes restent distinguables, quoi que fasse le thème. Le combo est toujours plein, la précision toujours pointillée, la densité toujours une aire ou des barres (ce n'est pas réglable). Côté couleur, le combo, la précision et le milieu de la densité doivent différer deux à deux d'au moins 30° de teinte (pour des gris : 25 % de luminosité) ; sinon le thème entier est refusé au chargement, avec le nom des deux jetons. Les couleurs des paliers de jugement et ratingColor ne sont jamais des jetons ; --pv-rating-dark et --pv-tier-dark ne font qu'assombrir leur texte.

Les tailles du jeu (espacements --pv-s*, durées --pv-dur*, --pv-stagger) appartiennent à l'application ; sous prefers-reduced-motion les durées et --pv-shift-base tombent à zéro quel que soit le thème.

L'illustration de la map reste visible, quel que soit le thème. L'image de la map derrière les menus fait partie de l'identité du jeu : un thème peut la teinter (--pv-shade), l'assombrir, la flouter, la désaturer ou la recolorer (--pv-art-filter), jamais la retirer ni l'enterrer. Le validateur (crates/themes/src/artwork.rs) calcule la part de lumière de l'image qui atteint l'écran, en moyenne sur la fenêtre : chaque dégradé de --pv-shade compte pour la moyenne de son opacité le long de son axe, les couches se composent, et le filtre compte pour ses brightness() et opacity(). Les règles : au moins 20 % de l'image visible ; aucune couche de --pv-shade ne couvre plus de 90 % en moyenne (un aplat opaque est refusé) ; dans le filtre, brightness ≥ 0,25, opacity ≥ 0,4 et contrast ≥ 0,5. Le contrôle s'applique aussi à chaque bloc @media. Le joueur garde son interrupteur : « Fond noir » (Vidéo → Fond en jeu) masque l'image en jeu ; la sélection de morceaux n'est pas concernée, et la luminosité du fond (backgroundBrightness) ne change pas non plus. Pour des plaques lisibles sans cacher l'image, préférer des plaques translucides (opacité de 0,7 à 0,85).

Un thème ne change pas la géométrie d'un bouton-icône. Un bouton-icône (IconButton, les « ? » d'aide, les icônes d'export et d'import) est un carré dont le côté vient des jetons --pv-h-md (--pv-h-sm pour la petite taille) et dont la forme vient de --pv-clip-sm ; l'icône est centrée dedans (display: inline-grid; place-items: center, svg en bloc), et ni la casse, ni l'interlettrage, ni la hauteur de ligne, ni le remplissage latéral que les thèmes donnent aux boutons de texte ne l'atteignent. Un thème en capitales, en mono ou espacé ne peut donc pas décentrer ni déformer une icône. Les boutons de texte (Utiliser, Personnaliser…) restent libres d'avoir le style du thème. Garde-fou : tests/icon-button-geometry.spec.ts parcourt tous les thèmes livrés, à 1400×850 et 1000×700, et vérifie que chaque bouton-icône de la page Skins est un carré, que son icône est centrée à ±0,5 px, qu'elle ne déborde pas, que les boutons d'une même rangée ont la même hauteur, et que le survol, le focus clavier et l'appui ne changent pas la taille.

Écrire un quatrième thème

  1. Copier themes/slant/ en themes/<mon-id>/ (ou dans <data>/themes).
  2. Éditer theme.json (id = nom du dossier) et tokens.css.
  3. Dans Skins → Recharger (ou relancer), la carte du thème apparaît ; l'aperçu est dessiné avec vos jetons.

Pour changer la construction des écrans, ajouter des mots --pv-layout-* dans tokens.css (voir « Choix de mise en page ») ; un mot omis garde la mise en page actuelle. Les thèmes livrés servent d'exemples : macos (rail d'onglets, bandeau de fenêtre), hack (crochets, focus clignotant, dense), ledger (rail, profil en colonne, tiroir à droite). Les jetons --pv-tier-dark et --pv-rating-dark servent à un thème clair (texte des paliers assombri).

Rien d'autre : pas de code, pas d'enregistrement. Le thème par défaut (réglage uiTheme absent ou invalide) est cabinet ; le thème d'identifiant classic reste choisissable et est réservé à la structure d'origine des menus (les composants d'avant le système de design lui sont rattachés par cet identifiant) ; un autre identifiant reçoit la structure du système de design.

Repli

Le thème choisi est absent : le premier thème disponible est utilisé, avec un avis. Aucun thème n'existe : l'application utilise ses jetons neutres intégrés (theme/fallback.css), de sorte que rien n'est jamais affiché sans style.

L'éditeur de skin

L'éditeur de skin (Skins → Personnaliser) vit dans la surcouche, une page WebView à part qui n'a ni la liste des thèmes ni les réglages : l'hôte lui envoie overlayTheme (le thème choisi uiTheme, accentSource, accentCustom et les thèmes déjà validés, les mêmes données que interfaces, aucune de plus et rien qu'un script puisse atteindre). La page en tire, avec le même code que les menus (theme/apply.ts, direction.ts, accent.ts), les variables CSS sous [data-direction], les mots data-layout-*, data-scheme et l'accent du joueur (un thème accent: fixed garde le sien). Sur la page, PrismLegacy est déclarée comme dans les menus pour les piles de polices qui la nomment.

Le panneau, les onglets, les champs, les interrupteurs, la barre d'outils et les cadres de l'éditeur ne lisent que ces jetons, par une couche de variables --ed-* (apps/web/src/overlay/editor/editor-theme.css) : surfaces (--pv-plate, --pv-bg, --pv-plate-raised), lignes (--pv-line), textes (--pv-text, -2, -3), accent et encre (--pv-accent, --pv-ink), sélection (--pv-accent-2, à défaut l'accent), rayons (--pv-radius-input, --pv-radius-panel), polices (--pv-font-body, -display, -label, -num), --pv-switch et --pv-thumb avec le mot --pv-layout-controls: pill (interrupteur en pilule, onglets en capsule à pouce relevé). Aucune couleur n'est écrite en dur dans l'éditeur (un test le vérifie) : un thème ajouté plus tard le restyle sans changement. Seuls le voile sur le jeu, les ombres et l'espace colorimétrique du sélecteur de couleur sont neutres.

Source dans le dépôt du jeu : docs/themes.md