import html as html_lib import db from .style_string import _style_string from .auto_id import _auto_id def _hex_to_rgb(hex_color): hex_color = (hex_color or "#5b8cff").lstrip("#") if len(hex_color) != 6: hex_color = "5b8cff" return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) def _interpolate_color(low_hex, high_hex, ratio): """Dégradé linéaire simple entre deux couleurs (rouge en bas, vert en haut par défaut, comme demandé dans le document d'interface de Boîte Mail) — ratio est déjà borné entre 0 et 1 par l'appelant.""" lo, hi = _hex_to_rgb(low_hex), _hex_to_rgb(high_hex) mixed = tuple(round(lo[i] + (hi[i] - lo[i]) * ratio) for i in range(3)) return "#{:02x}{:02x}{:02x}".format(*mixed) def _read_field_value(slug, definition_id, row_id, field_name): """Lit la valeur du champ demandé sur l'enregistrement visé (row_id) de l'objet suivi — quand un objet contient plusieurs enregistrements (ex: Réputation/Trésorerie/Confiance dans un même objet "jauge"), row_id est ce qui distingue lequel suivre. Repli sur la ligne la PLUS RÉCENTE si row_id est absent ou introuvable (jauges créées avant ce réglage, ou ligne supprimée depuis) — comportement historique, jamais de régression pour un objet à une seule ligne.""" if not definition_id or not field_name or not slug: return None definition = db.get_definition(slug, int(definition_id)) if not definition: return None rows = db.list_rows(slug, definition) if not rows: return None row = None if row_id: row = next((r for r in rows if str(r["id"]) == str(row_id)), None) if row is None: row = rows[0] col = db.slugify(field_name).replace("-", "_") return row.get(col) def _render_jauge(el, meta, slug, children_map=None, ctx=None, parent_flex_direction=None): """Jauge/barre dont le remplissage (largeur en %) et la couleur (dégradé bas -> haut) se recalculent automatiquement à partir d'un champ numérique d'un objet de données — voir 2.1 dans la doc de cadrage Forge Engine ("binding live d'une donnée vers un style"). Recalculée à chaque régénération du HTML du jeu (chargement de /play, et après toute action "Modifier une donnée"), donc toujours à jour sans action supplémentaire à poser dans le graphe de logique.""" attrs = el.get("attributes") or {} nom_position = attrs.get("_nom_position") or "dessus" nom_alignement = attrs.get("_nom_alignement") or "centre" nom_police = attrs.get("_nom_police") or "inherit" nom_taille = attrs.get("_nom_taille") or "12" nom_gras = attrs.get("_nom_gras") == "1" nom_italique = attrs.get("_nom_italique") == "1" # display:flex — le nom (optionnel) s'empile AU-DESSUS de la barre (mode # "dessus", flex-direction:column) ou se place à sa gauche (mode "cote", # flex-direction:row) plutôt que de se superposer dedans — voir name_html # plus bas. "justify-content:center" recentre le bloc nom+barre quand la # jauge se retrouve avec une hauteur supérieure à son contenu (ex. héritée # d'un conteneur parent qui l'étire) — sans ça le bloc restait collé en # haut de sa propre boîte, donnant une impression de centrage faussé dès # qu'un nom (donc plus de contenu) était ajouté. Le fond/bordure/overflow # vivent sur le wrapper de la barre (bar_wrap_style), pas ici, pour que le # nom reste en dehors de la zone rognée par "overflow:hidden". flex_dir = "row" if nom_position == "cote" else "column" align_items = "center" if nom_position == "cote" else "stretch" style = _style_string( el, f" display:flex; flex-direction:{flex_dir}; align-items:{align_items}; justify-content:center; gap:4px;", parent_flex_direction, ) definition_id = attrs.get("_definition_id") row_id = attrs.get("_row_id") field_name = (attrs.get("_champ") or "").strip() field_name_nom = (attrs.get("_champ_nom") or "").strip() try: min_v = float(attrs.get("_min", "0") or 0) except (TypeError, ValueError): min_v = 0.0 try: max_v = float(attrs.get("_max", "100") or 100) except (TypeError, ValueError): max_v = 100.0 couleur_basse = attrs.get("_couleur_basse") or "#e05252" couleur_haute = attrs.get("_couleur_haute") or "#4caf50" afficher_valeur = attrs.get("_afficher_valeur") == "1" raw_value = _read_field_value(slug, definition_id, row_id, field_name) if field_name else None try: value = float(raw_value) except (TypeError, ValueError): value = min_v span = (max_v - min_v) or 1 ratio = max(0.0, min(1.0, (value - min_v) / span)) percent = round(ratio * 100, 1) fill_color = _interpolate_color(couleur_basse, couleur_haute, ratio) label_html = "" if afficher_valeur: label_html = ( f'
{html_lib.escape(str(raw_value if raw_value is not None else "—"))}
' ) name_html = "" if field_name_nom: nom_value = _read_field_value(slug, definition_id, row_id, field_name_nom) if nom_value not in (None, ""): text_align = "left" if (nom_position == "dessus" and nom_alignement == "gauche") else "center" name_html = ( f'
{html_lib.escape(str(nom_value))}
' ) # Vraie balise Bulma ("class=progress") plutôt qu'une barre # maison faite de
positionnés — la couleur dégradée bas/haut (voir # _interpolate_color ci-dessus) reste possible nativement : Bulma pilote # justement le remplissage via la variable CSS --bulma-progress-value- # background-color, qu'on peut fixer en ligne à la couleur calculée sans # sortir du système de classes de Bulma. La classe "jaugeBar" est # repérée côté client (play.html, refreshRuntimeData) pour savoir que # cette balise doit être régénérée après toute action "Modifier une # donnée" — sinon la jauge resterait figée sur sa valeur de départ après # un changement, exactement comme il fallait déjà repérer "repeaterItem" # pour les Répéteurs (voir 1.1/1.2). extra_classes = " ".join(v for k, v in attrs.items() if k.startswith("_class_") and v) bar_class = html_lib.escape((f"progress jaugeBar {extra_classes}").strip()) bar = ( f'{percent}%' ) # min-height (pas 0) : un élément posé DANS un conteneur a une hauteur # "auto" par défaut (voir _style_string) — "flex:1 1 auto" seul n'a # alors rien à répartir (le conteneur flex lui-même n'a pas de hauteur # définie tant que rien ne la contraint) et la barre s'effondrait à 0px, # ne laissant plus que le nom visible. Cette valeur plancher garde la # barre visible dans ce cas, tout en la laissant grandir avec flex:1 si # l'élément a une vraie hauteur (posé directement sur un écran, ou # hauteur fixée via "Position & taille"). bar_wrap = ( f'
{bar}{label_html}
' ) return ( f'
' f'{name_html}{bar_wrap}
' )