diff --git a/filters/element_style_filter.py b/filters/element_style_filter.py index 4618102b..c824ba82 100644 --- a/filters/element_style_filter.py +++ b/filters/element_style_filter.py @@ -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) + ";" diff --git a/screens/element_types/is_overlay_only.py b/screens/element_types/is_overlay_only.py new file mode 100644 index 00000000..bc611662 --- /dev/null +++ b/screens/element_types/is_overlay_only.py @@ -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"} diff --git a/screens/elements/list_elements.py b/screens/elements/list_elements.py index cc755cae..f047648c 100644 --- a/screens/elements/list_elements.py +++ b/screens/elements/list_elements.py @@ -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 diff --git a/screens/rendering/render_element_html.py b/screens/rendering/render_element_html.py index 909e205d..bf8dcd3e 100644 --- a/screens/rendering/render_element_html.py +++ b/screens/rendering/render_element_html.py @@ -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"]) diff --git a/tests/test_confort.py b/tests/test_confort.py index 983e146c..ab53612e 100644 --- a/tests/test_confort.py +++ b/tests/test_confort.py @@ -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('