Dev #4

Merged
w-vandal merged 16 commits from dev into main 2026-08-29 04:02:11 +00:00
5 changed files with 102 additions and 7 deletions
Showing only changes of commit 6e46949cf8 - Show all commits
+13 -2
View File
@@ -41,13 +41,24 @@ def _element_style(el):
SEUL le z-index est omis pour ce widget précis. position:absolute SANS
z-index explicite (donc z-index:auto) ne crée PAS de nouveau contexte
d'empilement — le z-index:9999 posé plus profond se compare alors
directement aux autres éléments de l'écran, et gagne toujours."""
directement aux autres éléments de l'écran, et gagne toujours.
Même omission pour un exemplaire (element_type_id) d'un élément de jeu
dont le modèle N'EST QU'une superposition (marqué "_overlay_only_instance"
par list_elements.py) : son propre widget est "conteneur" par défaut
(add_element.py), pas "superposition", mais son cadre entoure quand
même une superposition rendue à l'intérieur (voir
render_element_html.py/_render_element_type_children) — exactement le
même risque de contexte d'empilement piégeant."""
exempt_from_z_index = (
el.get("widget") == "superposition" or el.get("_overlay_only_instance")
)
parts = [
"position:absolute",
f"left:{el['x']}%", f"top:{el['y']}%",
f"width:{el['width']}%", f"height:{el['height']}%",
]
if el.get("widget") != "superposition":
if not exempt_from_z_index:
parts.append(f"z-index:{el['z_index']}")
return "; ".join(parts) + ";"
+16
View File
@@ -0,0 +1,16 @@
from .load_template_tree import _load_template_tree
def _is_overlay_only_element_type(slug, element_type_id):
"""True si le modèle d'un élément de jeu N'EST QU'une "Superposition /
boîte de dialogue" (un seul élément de premier niveau, de ce widget) —
utilisé à deux endroits qui doivent s'accorder : render_element_html.py
(court-circuite l'enveloppe générique "conteneur" d'un exemplaire, qui
resterait sinon visible en permanence) et element_style_filter.py (le
cadre .canvasElement/.playElement de cet exemplaire ne doit pas non
plus poser de z-index, pour la même raison qu'une superposition posée
directement — voir son propre commentaire)."""
template_tree = _load_template_tree(slug, element_type_id)
if not template_tree:
return False
return {c.get("widget") for c in template_tree["top"]} == {"superposition"}
+10
View File
@@ -3,6 +3,7 @@ import json
import db
from ..rendering.render_element_html import render_element_html
from ..element_types.is_overlay_only import _is_overlay_only_element_type
def list_elements(slug, screen_id, enforce_visibility=False):
@@ -75,5 +76,14 @@ def list_elements(slug, screen_id, enforce_visibility=False):
children_map.setdefault(d["parent_id"], []).append(d)
play_ctx = {"_forge_play_mode": True} if enforce_visibility else None
for d in result:
# Un exemplaire de premier niveau d'un élément de jeu dont le
# modèle N'EST QU'une superposition : son cadre de positionnement
# (.canvasElement/.playElement, voir filters/element_style_filter.py)
# ne doit pas non plus poser de z-index, pour la même raison qu'une
# superposition posée directement (sinon elle crée un contexte
# d'empilement CSS qui piège le z-index:9999 de la superposition
# rendue à l'intérieur — voir element_style_filter.py).
if d.get("element_type_id") and not d.get("parent_id"):
d["_overlay_only_instance"] = _is_overlay_only_element_type(slug, d["element_type_id"])
d["rendered_html"] = render_element_html(d, slug, children_map, play_ctx)
return result
+28 -5
View File
@@ -18,6 +18,7 @@ from .render_icone import _render_icone
from .resolve_bound_row import _resolve_bound_row_ctx
from .visibility_condition import is_element_visible
from ..element_types.load_template_tree import _load_template_tree
from ..element_types.is_overlay_only import _is_overlay_only_element_type
def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None):
@@ -107,9 +108,28 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
return _mark(f"<{tag} {_attr_string(attrs, style)}>")
content = _apply_ctx(el.get("content") or "", ctx)
child_html = _render_element_type_children(el, slug, ctx) if el.get("element_type_id") else None
child_html, is_overlay_template = (
_render_element_type_children(el, slug, ctx) if el.get("element_type_id") else (None, False)
)
if child_html is None:
child_html = _render_children(el, slug, children_map, ctx)
elif is_overlay_template:
# Un exemplaire d'élément de jeu est posé par défaut avec le widget
# générique "conteneur" (add_element.py, "default_widget") — utile
# pour la plupart des modèles, mais QUAND le modèle entier n'est
# qu'une "Superposition / boîte de dialogue", cette enveloppe
# (fond, bordure, position normale sur le canevas — voir
# widgets/registry.py "conteneur") resterait visible EN PERMANENCE
# à l'endroit où l'exemplaire a été déposé, alors que la
# superposition à l'intérieur gère déjà entièrement sa propre
# apparence et son propre masquage (position:fixed plein écran,
# démarre masquée) — vécu comme "un conteneur vide apparaît sur la
# scène, pas la boîte de dialogue" (en réalité la boîte de dialogue
# existe bien, juste masquée comme prévu ; c'est le conteneur
# AUTOUR qui n'aurait jamais dû être visible). On court-circuite
# donc entièrement l'enveloppe et on renvoie directement le
# contenu du modèle.
return _mark(child_html)
if tag in ("ul", "ol"):
items = [line.strip() for line in content.split("\n") if line.strip()]
@@ -128,15 +148,18 @@ def _render_element_type_children(el, slug, ctx):
Répéteur (voir _render_repeater/_load_template_tree). Modifier
l'élément de jeu dans son propre éditeur met donc à jour tous ses
exemplaires déjà posés, sur n'importe quel écran, sans avoir à les
retoucher un par un. Renvoie None (pas "") si l'élément de jeu n'a
retoucher un par un. Renvoie (None, False) si l'élément de jeu n'a
plus de modèle valide, pour que l'appelant retombe sur le rendu
générique (d'éventuels enfants en base issus d'une version antérieure
de ce mécanisme) plutôt que d'afficher un exemplaire silencieusement
vide."""
vide. Le second élément renvoyé indique si le modèle N'EST QUE une
superposition (voir l'appelant : dans ce cas précis, l'enveloppe
générique "conteneur" de l'exemplaire doit être court-circuitée)."""
template_tree = _load_template_tree(slug, el["element_type_id"])
if not template_tree:
return None
return "".join(
return None, False
html = "".join(
render_element_html(c, slug, template_tree["children_map"], ctx)
for c in template_tree["top"]
)
return html, _is_overlay_only_element_type(slug, el["element_type_id"])
+35
View File
@@ -232,6 +232,41 @@ def test_overlay_stays_visible_in_the_editor_despite_starting_masked(client, gam
assert wrapper_style2.rstrip().endswith("display:none;")
def test_overlay_element_type_instance_has_no_visible_wrapper_box(client, game):
"""Régression : poser un élément de jeu réutilisable ("Mes éléments de
jeu") dont le MODÈLE n'est qu'une superposition affichait, sur la
vraie scène, une boîte "conteneur" vide et TOUJOURS VISIBLE à
l'endroit où l'exemplaire a été déposé — "un conteneur vide apparaît,
pas la boîte de dialogue" (elle existe bien, masquée comme prévu ;
c'est l'enveloppe générique "conteneur" AUTOUR, posée par défaut pour
tout exemplaire (add_element.py), qui n'aurait jamais dû être visible
pour un modèle qui n'est QUE ça). Corrigé en court-circuitant cette
enveloppe dès que le modèle entier est une superposition."""
import screens
resp = client.post(f"/game/{game}/element-types", data={"name": "Dialogue"}, follow_redirects=False)
assert resp.status_code == 302
et = next(t for t in screens.list_element_types(game) if t["name"] == "Dialogue")
template_screen_id = et["template_screen_id"]
overlay_id = _add_element(client, game, template_screen_id, "superposition")
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
screen_id = _create_screen(client, game, "Scène")
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add",
data={"widget": "__catalogue__", "element_type_id": et["id"]}, follow_redirects=False)
assert resp.status_code == 302
# Un frère ajouté APRÈS -> z_index plus grand, comme dans le scénario
# rapporté (une scène avec d'autres éléments déjà en place).
_add_element(client, game, screen_id, "bouton")
html = client.get(f"/game/{game}/play").data.decode()
assert "forgeOverlayBox" in html
assert 'class="box"' not in html # l'enveloppe "conteneur" par défaut ne doit plus apparaître
idx = html.find('class="modal is-active"')
wrapper_start = html.rfind('<div class="playElement"', 0, idx)
wrapper_style = re.search(r'style="([^"]*)"', html[wrapper_start:idx]).group(1)
assert "z-index" not in wrapper_style
def test_overlay_wrapper_does_not_trap_its_own_z_index(client, game):
"""Régression : le cadre .playElement/.canvasElement partagé par TOUS
les widgets (voir filters/element_style_filter.py) posait quand même