Files
Forge-Engine/screens/widgets/registry.py
T
william bb01e9fdf1 Make Jauge, Onglets, and every form-field widget real native Bulma elements
- Jauge now renders a real <progress class="progress"> instead of a hand-built pair of absolutely-positioned divs. The bas/haut color interpolation still works, set via Bulma's own --bulma-progress-value-background-color CSS custom property rather than fighting the class.
- Onglets' tab strip is now genuine Bulma tabs markup (tabs > ul > li, is-active on the li) instead of custom forgeTabBar/forgeTabBtn classes; forgeShowTab (duplicated in screen_edit.html and play.html) now toggles is-active to match.
- Champ texte/email/mot de passe get class="input", Zone de texte gets class="textarea", Case à cocher/Bouton radio's existing <label> wrapper gets class="checkbox"/"radio" (Bulma's own convention — the structure already matched, just needed the class), Liste déroulante is wrapped in Bulma's required <div class="select"> (a bare class on the <select> itself has zero effect in Bulma), Tableau gets class="table is-bordered is-fullwidth" with the per-cell inline borders removed so Bulma's own table styling applies.
- Removed the now-dead .forgeTabBar/.forgeTabBtn CSS.

Updated tests/test_jauge.py and tests/test_onglets_widget.py assertions to match the new markup (value="X" attribute instead of width:X% inline style, --bulma-progress-value-background-color instead of background-color, forgeShowTab(this) marker instead of the removed forgeTabBtn class) — same behavior, different rendering mechanism. Full suite green (89 passed), verified end to end against the real "test" project via the actual /play route.
2026-08-24 14:31:55 +02:00

316 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from .controls.c_text import c_text
from .controls.c_textarea import c_textarea
from .controls.c_url import c_url
from .controls.c_color import c_color
from .controls.c_slider import c_slider
from .controls.c_checkbox import c_checkbox
from .controls.c_toggle_token import c_toggle_token
from .controls.c_align import c_align
from .controls.c_select import c_select
from .control_groups.text_style_controls_extra import TEXT_STYLE_CONTROLS_EXTRA
from .control_groups.data_binding_controls import DATA_BINDING_CONTROLS
from .control_groups.bulma_controls import BULMA_BUTTON_CONTROLS
from .font_options import FONT_OPTIONS
from ..flow.constants import CONDITION_OPERATORS
WIDGETS = {
"texte": {
"label": "Texte", "icon": "📝", "tag": "p", "has_content": True,
"w": 40, "h": 10, "default_content": "Texte du paragraphe.",
"controls": [
c_textarea("content", "Texte — {{champ}} affiche la valeur de ce champ sur la ligne trouvée ci-dessous", "content", default="Texte du paragraphe.", group="Contenu"),
] + DATA_BINDING_CONTROLS + [
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=64, default=16, group="Texte"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_align("text-align", "Alignement", "style:text-align", group="Texte"),
c_checkbox("bold", "Gras", "style:font-weight", "bold", "normal", group="Texte"),
c_checkbox("italic", "Italique", "style:font-style", "italic", "normal", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
c_toggle_token("strikethrough", "Barré", "style:text-decoration", "line-through", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"titre": {
"label": "Titre", "icon": "🔠", "tag": None, "has_content": True,
"w": 55, "h": 10, "default_content": "Titre",
# class:"title" — poids de police et espacement Bulma par défaut ; la
# taille reste pilotée par le réglage "Taille du texte" ci-dessous
# comme avant (non affecté), Bulma ne fournissant ici que l'habillage.
"fixed_attrs": {"class": "title"},
"controls": [
c_textarea("content", "Texte — {{champ}} affiche la valeur de ce champ sur la ligne trouvée ci-dessous", "content", default="Titre", group="Contenu"),
c_select("niveau", "Importance du titre", "attr:_niveau",
[("1", "Très grand — titre principal"), ("2", "Grand"), ("3", "Moyen")], default="2", group="Contenu"),
] + DATA_BINDING_CONTROLS + [
c_slider("font-size", "Taille du texte", "style:font-size", min=14, max=80, default=32, group="Texte"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_align("text-align", "Alignement", "style:text-align", default="left", group="Texte"),
c_checkbox("italic", "Italique", "style:font-style", "italic", "normal", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"bouton": {
"label": "Bouton", "icon": "🔘", "tag": "button", "has_content": True,
"w": 30, "h": 8, "default_content": "Valider",
# class:"button" — donne au Bouton l'apparence Bulma par défaut
# (padding, poids de police, transitions...) tant qu'aucune couleur/
# taille "libre" n'est personnalisée ci-dessous — voir
# BULMA_BUTTON_CONTROLS et _visible_attrs.py.
"fixed_attrs": {"class": "button"},
"controls": [
c_text("content", "Texte du bouton", "content", default="Valider", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#ffffff", group="Texte"),
c_color("background", "Couleur de fond", "style:background-color", "#5b8cff", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=15, group="Texte", no_freeze=True),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=40, default=8, group="Texte", no_freeze=True),
c_checkbox("submit", "Valide un formulaire", "attr:type", "submit", "button", group="Contenu"),
] + BULMA_BUTTON_CONTROLS + TEXT_STYLE_CONTROLS_EXTRA,
},
"lien": {
"label": "Lien", "icon": "🔗", "tag": "a", "has_content": True,
"w": 25, "h": 6, "default_content": "Cliquez ici",
"controls": [
c_text("content", "Texte du lien", "content", default="Cliquez ici", group="Contenu"),
c_url("href", "Destination", "attr:href", default="#", group="Contenu"),
c_checkbox("new_tab", "Ouvrir dans un nouvel onglet", "attr:target", "_blank", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#5b8cff", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"image": {
"label": "Image", "icon": "🖼️", "tag": "img", "has_content": False, "self_closing": True,
"w": 35, "h": 22, "default_content": "",
"controls": [
c_url("src", "Source de l'image", "attr:src", allow_upload=True, upload_accept="image/*", group="Contenu"),
c_text("alt", "Texte alternatif", "attr:alt", placeholder="décrit l'image pour l'accessibilité", group="Contenu"),
c_select("object-fit", "Ajustement dans son cadre", "style:object-fit",
[("cover", "Remplir en recadrant"), ("contain", "Tout montrer sans déformer"), ("fill", "Étirer pour remplir tout le cadre")],
default="cover", group="Média"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=60, default=0, group="Média"),
],
},
"video": {
"label": "Vidéo", "icon": "🎬", "tag": "video", "has_content": False,
"w": 40, "h": 25, "default_content": "",
"controls": [
c_url("src", "Source de la vidéo", "attr:src", allow_upload=True, upload_accept="video/*", group="Contenu"),
c_checkbox("controls", "Afficher les contrôles de lecture", "attr:controls", "controls", "", default=True, group="Contenu"),
c_checkbox("autoplay", "Lecture automatique", "attr:autoplay", "autoplay", "", group="Contenu"),
c_checkbox("loop", "Lecture en boucle", "attr:loop", "loop", "", group="Contenu"),
],
},
"champ_texte": {
"label": "Champ — texte", "icon": "⌨️", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "text", "class": "input"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre réponse", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. reponse1", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"champ_email": {
"label": "Champ — email", "icon": "📧", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "email", "class": "input"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. votre@email.com", default="votre@email.com", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. email", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"champ_mot_de_passe": {
"label": "Champ — mot de passe", "icon": "🔒", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "password", "class": "input"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Mot de passe", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. mot_de_passe", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"case_a_cocher": {
"label": "Case à cocher", "icon": "☑️", "tag": "input", "has_content": True, "special_render": "checkbox_label",
"w": 35, "h": 8, "default_content": "Option",
"controls": [
c_text("content", "Texte affiché", "content", default="Option", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. accepte_cgu", group="Contenu"),
c_text("value", "Valeur envoyée", "attr:value", placeholder="ex. oui", group="Contenu"),
c_checkbox("checked", "Coché par défaut", "attr:checked", "checked", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"bouton_radio": {
"label": "Bouton radio", "icon": "🔘", "tag": "input", "has_content": True, "special_render": "checkbox_label",
"w": 35, "h": 8, "default_content": "Option",
"controls": [
c_text("content", "Texte affiché", "content", default="Option", group="Contenu"),
c_text("name", "Nom du groupe", "attr:name", placeholder="ex. choix1", group="Contenu"),
c_text("value", "Valeur envoyée", "attr:value", placeholder="ex. optionA", group="Contenu"),
c_checkbox("checked", "Sélectionné par défaut", "attr:checked", "checked", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"zone_texte": {
"label": "Zone de texte", "icon": "🗒️", "tag": "textarea", "has_content": False,
"w": 40, "h": 18, "default_content": "", "fixed_attrs": {"class": "textarea"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre message", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. message", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"liste_deroulante": {
"label": "Liste déroulante", "icon": "🔽", "tag": "select", "has_content": True, "special_render": "select_options",
"w": 35, "h": 8, "default_content": "Option 1\nOption 2\nOption 3",
"controls": [
c_textarea("content", "Options — une par ligne", "content", default="Option 1\nOption 2\nOption 3", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. choix", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"groupe_champs": {
"label": "Groupe de champs", "icon": "🗃️", "tag": "fieldset", "has_content": True, "special_render": "fieldset_legend",
"w": 55, "h": 30, "default_content": "Groupe de champs",
"controls": [
c_text("content", "Titre du groupe", "content", default="Groupe de champs", group="Contenu"),
],
},
"liste": {
"label": "Liste à puces", "icon": "📋", "tag": "ul", "has_content": True,
"w": 40, "h": 22, "default_content": "Premier élément\nDeuxième élément\nTroisième élément",
"controls": [
c_textarea("content", "Éléments de la liste — un par ligne", "content",
default="Premier élément\nDeuxième élément\nTroisième élément", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
],
},
"liste_numerotee": {
"label": "Liste numérotée", "icon": "🔢", "tag": "ol", "has_content": True,
"w": 40, "h": 22, "default_content": "Premier élément\nDeuxième élément\nTroisième élément",
"controls": [
c_textarea("content", "Éléments de la liste — un par ligne", "content",
default="Premier élément\nDeuxième élément\nTroisième élément", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
],
},
"tableau": {
"label": "Tableau", "icon": "🗂️", "tag": "table", "has_content": True, "special_render": "table",
"w": 55, "h": 25, "default_content": "Colonne 1;Colonne 2\nValeur A;Valeur B",
"fixed_attrs": {"class": "table is-bordered is-fullwidth"},
"controls": [
c_textarea("content", "Contenu — une ligne par rangée, colonnes séparées par ;", "content",
default="Colonne 1;Colonne 2\nValeur A;Valeur B", group="Contenu"),
c_checkbox("header_row", "Première ligne = en-tête", "attr:_header_row", "1", "", default=True, group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"conteneur": {
"label": "Conteneur / décor", "icon": "⬛", "tag": "div", "has_content": True,
"w": 50, "h": 30, "default_content": "",
# class:"box" — ce widget EST une carte Bulma nativement (fond clair,
# ombre, coins arrondis), pas un habillage qu'on choisit dans un
# menu : voir bouton/titre ci-dessus, même logique. Une couleur de
# fond personnalisée (réglage ci-dessous) reste prioritaire dès
# qu'elle est choisie, exactement comme avant.
"fixed_attrs": {"class": "box"},
"controls": [
c_text("content", "Texte", "content", group="Contenu"),
c_color("background", "Couleur de fond", "style:background-color", "#1f2430", group="Texte"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=60, default=10, group="Texte", no_freeze=True),
c_slider("opacity", "Opacité", "style:opacity", unit="", min=0, max=1, step=0.05, default=1, group="Texte"),
],
},
"separateur": {
"label": "Séparateur", "icon": "➖", "tag": "hr", "has_content": False, "self_closing": True,
"w": 50, "h": 2, "default_content": "",
"controls": [
c_color("color", "Couleur", "style:border-color", "#2b3140", group="Texte"),
],
},
"repeteur_donnees": {
"label": "Répéteur de données", "icon": "🔁", "tag": "div", "has_content": True, "special_render": "repeater",
"w": 55, "h": 35, "default_content": "{{nom}}",
"controls": [
c_select("definition_id", "Objet de données à afficher", "attr:_definition_id",
[("", "Sélectionner un objet")], default="", group="Contenu"),
c_select("template_type_id", "Modèle de ligne — élément de jeu", "attr:_template_type_id",
[("", "Aucun — utiliser le modèle de texte ci-dessous")], default="", group="Contenu"),
c_textarea("content", "Modèle d'affichage texte — utilise {{nom_du_champ}}", "content", default="{{nom}}", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
c_text("filtre_champ", "Filtrer : n'afficher que les lignes où ce champ",
"attr:_filtre_champ", placeholder="ex. niveau", default="", group="Filtre"),
c_select("filtre_operateur", "...est", "attr:_filtre_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"),
c_text("filtre_valeur", "...cette valeur, ou {{NomObjet.champ}} pour comparer à une donnée qui change en cours de partie",
"attr:_filtre_valeur", placeholder="ex. 3 ou {{Partie.niveau_courant}}", default="", group="Filtre"),
c_text("filtre2_champ", "ET (optionnel) : ce 2ᵉ champ",
"attr:_filtre2_champ", placeholder="ex. parcour_id", default="", group="Filtre"),
c_select("filtre2_operateur", "...est", "attr:_filtre2_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"),
c_text("filtre2_valeur", "...cette valeur, ou {{NomObjet.champ}}",
"attr:_filtre2_valeur", placeholder="ex. 1 ou {{Partie.parcours_courant}}", default="", group="Filtre"),
],
},
"jauge": {
"label": "Jauge — liée à une donnée", "icon": "📊", "tag": "div", "has_content": False, "special_render": "jauge",
"w": 60, "h": 12, "default_content": "",
"controls": [
# La clé "definition_id" est reconnue spécifiquement dans
# controls_with_values.py pour être peuplée avec les VRAIS
# objets de données du jeu — même mécanisme que le Répéteur.
c_select("definition_id", "Objet de données à suivre", "attr:_definition_id",
[("", "Sélectionner un objet")], default="", group="Donnée"),
# row_id/champ/champ_nom : options peuplées dynamiquement une
# fois "definition_id" choisi — voir controls_with_values.py
# (premier affichage) et onJaugeDefinitionChange() dans
# screen_edit.html (changement en direct dans le panneau).
c_select("row_id", "Enregistrement (ligne) à suivre", "attr:_row_id",
[("", "Sélectionner un objet d'abord")], default="", group="Donnée"),
c_select("champ", "Champ numérique à afficher", "attr:_champ",
[("", "Sélectionner un objet d'abord")], default="", group="Donnée"),
c_select("champ_nom", "Champ contenant le nom (affiché au-dessus, optionnel)", "attr:_champ_nom",
[("", "— aucun —")], default="", group="Donnée"),
c_text("min", "Valeur mini", "attr:_min", default="0", group="Donnée"),
c_text("max", "Valeur maxi", "attr:_max", default="100", group="Donnée"),
c_color("couleur_basse", "Couleur côté mini", "attr:_couleur_basse", "#e05252", group="Apparence"),
c_color("couleur_haute", "Couleur côté maxi", "attr:_couleur_haute", "#4caf50", group="Apparence"),
c_checkbox("afficher_valeur", "Afficher la valeur en chiffres sur la jauge", "attr:_afficher_valeur", "1", "", default=False, group="Apparence"),
c_select("nom_position", "Position du nom", "attr:_nom_position",
[("dessus", "Au-dessus de la jauge"), ("cote", "À côté (à gauche) de la jauge")],
default="dessus", group="Apparence"),
c_select("nom_alignement", "Alignement du nom (si au-dessus)", "attr:_nom_alignement",
[("centre", "Centré"), ("gauche", "Aligné à gauche")],
default="centre", group="Apparence"),
c_select("nom_police", "Police du nom", "attr:_nom_police", FONT_OPTIONS, default="inherit", group="Apparence"),
c_slider("nom_taille", "Taille du texte du nom", "attr:_nom_taille", unit="", min=8, max=32, default=12, group="Apparence"),
c_checkbox("nom_gras", "Nom en gras", "attr:_nom_gras", "1", "", default=False, group="Apparence"),
c_checkbox("nom_italique", "Nom en italique", "attr:_nom_italique", "1", "", default=False, group="Apparence"),
],
},
"superposition": {
# 3.4 (Confort) — overlay/modale réutilisable : couvre TOUJOURS tout
# l'écran (position:fixed, indépendant de x/y/width/height — voir
# render_overlay.py), avec un voile semi-transparent + une boîte
# centrée qui contient les éléments posés à l'intérieur. Fermeture
# manuelle uniquement : réutilise l'action existante "Modifier un
# élément → Visibilité" (masquer/rendre visible), comme n'importe
# quel autre élément — pas de mécanisme de fermeture au clic externe.
"label": "Superposition / boîte de dialogue", "icon": "🪟", "tag": "div", "has_content": True,
"special_render": "overlay",
"w": 50, "h": 40, "default_content": "",
"controls": [
c_text("content", "Texte — en plus des éléments posés dans la boîte", "content", group="Contenu"),
c_color("couleur_boite", "Couleur de fond de la boîte", "attr:_couleur_boite", "#1f2430", group="Apparence"),
c_slider("arrondi", "Arrondi des coins de la boîte", "attr:_arrondi", unit="", min=0, max=40, default=12, group="Apparence"),
],
},
"onglets": {
# Chaque onglet est un vrai élément "conteneur" posé comme enfant
# (voir screens/elements/add_tab.py et render_onglets.py) — pas de
# gestion dédiée ici : ajouter/renommer/supprimer un onglet se fait
# via le bloc "Onglets" du panneau de propriétés (screen_edit.html),
# qui réutilise les routes existantes (element_rename, element_delete).
"label": "Onglets", "icon": "🗂️", "tag": "div", "has_content": False, "special_render": "onglets",
"w": 60, "h": 40, "default_content": "",
"controls": [],
},
}