Retire le voile plein écran et le forçage de visibilité de l'éditeur (retour utilisateur)
Deux retours après le dernier correctif (78a373a, qui forçait
"display:flex" dans l'éditeur pour que la boîte de dialogue reste
visible) :
- "je souhaite avoir la main sur la visibilité de la modale sinon elle
s'affiche toujours sur la scène, court-circuite ma logique" — le
forçage empêchait de vraiment utiliser "Visibilité" pendant l'édition.
- "quand j'édite la modale ou quand je la mets dans une scène je
souhaite que rien ne soit assombri, l'assombrissement ne se fait que
quand la scène est jouée" — le voile plein écran (position:fixed +
fond assombri) restait aussi actif dans l'éditeur.
Correctif (render_overlay.py) : le voile plein écran ET le forçage de
visibilité sont retirés de l'ÉDITEUR — ce widget s'y comporte maintenant
comme un CONTENEUR NORMAL (position/taille selon x/y/width/height, aucun
voile, réglage "Visibilité" respecté normalement, comme n'importe quel
autre widget masqué). Le comportement plein écran/voile/masquage par
défaut n'est conservé qu'en mode JOUABLE (ctx["_forge_play_mode"]).
En creusant pourquoi "Visible" ne suffisait pas à faire réapparaître la
boîte dans l'éditeur (menant l'utilisateur à essayer "Invisible" à la
place, visible dans ses captures), trouvé un vrai bug latent dans
save_element_controls.py : l'option "Visible" du réglage "Visibilité" ne
touche volontairement jamais "display" (pour ne pas écraser le
"display:flex" d'un conteneur en disposition ligne/colonne — voir
visibility_control.py) — ça fonctionne seulement parce que, pour un
widget AVEC un réglage "Disposition interne", celui-ci réaffirme lui-même
un display non-"none" au même enregistrement. La "superposition" n'a PAS
ce réglage : "display:none" (posé à la création ou par un "Masqué"
précédent) restait donc bloqué pour toujours, quel que soit le nombre de
fois où "Visible" était ensuite choisi. Corrigé : "Visible" efface aussi
"display" pour tout widget SANS réglage "Disposition interne" (safe : les
widgets qui EN ont un ne sont pas concernés, donc aucune régression sur
leur comportement existant).
Tests mis à jour (l'ancien test attendait le forçage, désormais retiré) +
nouveau test qui couvre le cycle complet (masqué par défaut -> "Visible"
choisi -> apparaît sans voile dans l'éditeur -> voile plein écran
retrouvé en mode jouable). 140 tests au vert au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6e46949cf8
commit
7c237d6f1c
@@ -3,6 +3,7 @@ import json
|
|||||||
import db
|
import db
|
||||||
|
|
||||||
from ..widgets.widget_meta import widget_meta
|
from ..widgets.widget_meta import widget_meta
|
||||||
|
from ..widgets.layout_capable_widgets import LAYOUT_CAPABLE_WIDGETS
|
||||||
from .get_element import get_element
|
from .get_element import get_element
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +62,24 @@ def save_element_controls(slug, element_id, form):
|
|||||||
style.pop(k, None)
|
style.pop(k, None)
|
||||||
else:
|
else:
|
||||||
style[k] = v
|
style[k] = v
|
||||||
|
if (
|
||||||
|
control["key"] == "visibilite" and chosen == "visible"
|
||||||
|
and el["widget"] not in LAYOUT_CAPABLE_WIDGETS
|
||||||
|
):
|
||||||
|
# "Visible" ne touche volontairement pas "display" (voir
|
||||||
|
# visibility_control.py) : sur un widget qui A un
|
||||||
|
# réglage "Disposition interne" (LAYOUT_CONTROLS,
|
||||||
|
# traité avant celui-ci sur le MÊME enregistrement,
|
||||||
|
# voir widget_meta.py), ce contrôle réaffirme lui-même
|
||||||
|
# un "display" non-"none" (flex/etc), donc "Visible" n'a
|
||||||
|
# rien à faire. Mais un widget SANS ce contrôle (ex.
|
||||||
|
# "superposition") n'a PERSONNE d'autre pour l'effacer :
|
||||||
|
# un "display:none" laissé par un "Masqué" précédent (ou
|
||||||
|
# par le réglage par défaut à la création, voir
|
||||||
|
# default_style_for_widget.py) restait donc bloqué
|
||||||
|
# masqué pour toujours, quel que soit le nombre de fois
|
||||||
|
# où "Visible" était ensuite choisi.
|
||||||
|
style.pop("display", None)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ctype == "scale":
|
if ctype == "scale":
|
||||||
|
|||||||
@@ -7,60 +7,61 @@ from .render_children import _render_children
|
|||||||
|
|
||||||
def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_direction=None):
|
def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_direction=None):
|
||||||
"""3.4 (Confort) — overlay/modale réutilisable : une boîte de dialogue
|
"""3.4 (Confort) — overlay/modale réutilisable : une boîte de dialogue
|
||||||
prête à l'emploi, par-dessus TOUT le reste de l'écran, fermeture
|
prête à l'emploi, par-dessus TOUT le reste de l'écran EN MODE JOUABLE
|
||||||
manuelle uniquement (aucun clic-en-dehors-pour-fermer volontairement,
|
UNIQUEMENT (`position:fixed; inset:0` + voile semi-transparent, ignore
|
||||||
conformément au manque documenté). Contrairement aux autres widgets, sa
|
x/y/width/height) — fermeture manuelle uniquement (aucun
|
||||||
position ne dépend PAS de x/y/width/height (glissé-déposé sur le
|
clic-en-dehors-pour-fermer volontairement, conformément au manque
|
||||||
canevas) : `position:fixed; inset:0` la fait toujours couvrir tout
|
documenté).
|
||||||
l'écran, quel que soit l'endroit où elle a été posée dans l'éditeur —
|
|
||||||
seul un voile semi-transparent + une boîte centrée, contenant les
|
|
||||||
éléments posés à l'intérieur (comme un conteneur normal).
|
|
||||||
|
|
||||||
Ouverture/fermeture : PAS de mécanisme dédié — elle réutilise l'action
|
Dans l'ÉDITEUR (ctx sans "_forge_play_mode" — posé uniquement en mode
|
||||||
existante "Modifier un élément → Visibilité" (masquer/rendre visible),
|
jouable, voir list_elements.py), ce widget se comporte comme un
|
||||||
exactement comme n'importe quel autre élément. C'est pour ça qu'on
|
CONTENEUR NORMAL : position/taille selon x/y/width/height comme
|
||||||
fusionne le style de positionnement fixe ci-dessous AVEC le style normal
|
n'importe quel widget, respect normal de son réglage "Visibilité"
|
||||||
de l'élément (_style_string) : si l'élément est réglé "Masqué" dans ses
|
(masqué = invisible dans l'éditeur aussi, comme tout autre widget),
|
||||||
propriétés, ou si une action "Modifier un élément" le cache plus tard,
|
AUCUN voile plein écran. Deux essais précédents corrigés à partir des
|
||||||
ce masquage continue de fonctionner normalement.
|
retours utilisateur :
|
||||||
|
- Fond assombri visible pendant l'édition ("je veux que rien ne soit
|
||||||
|
assombri, l'assombrissement ne se fait que quand la scène est
|
||||||
|
jouée") : le voile (background:rgba(...)) ne fait donc plus partie
|
||||||
|
du style de base, il n'est ajouté qu'en mode jouable.
|
||||||
|
- Visibilité forcée en permanence dans l'éditeur ("je veux avoir la
|
||||||
|
main sur la visibilité, sinon elle s'affiche toujours sur la scène
|
||||||
|
et court-circuite ma logique") : le forçage display:flex a donc été
|
||||||
|
retiré — l'éditeur respecte de nouveau fidèlement le réglage
|
||||||
|
"Visibilité" (masqué par défaut à la création, pour ne plus couvrir
|
||||||
|
tout l'écran EN JEU dès qu'on la pose — sans plus aucun rapport avec
|
||||||
|
l'éditeur, qui ne couvre plus jamais rien).
|
||||||
|
|
||||||
|
Ouverture/fermeture (en JEU) : PAS de mécanisme dédié — elle réutilise
|
||||||
|
l'action existante "Modifier un élément → Visibilité" (masquer/rendre
|
||||||
|
visible), exactement comme n'importe quel autre élément.
|
||||||
|
|
||||||
Classes Bulma ("modal is-active" / "box") posées en PLUS du style
|
Classes Bulma ("modal is-active" / "box") posées en PLUS du style
|
||||||
inline existant, jamais à sa place : tout le positionnement/masquage
|
inline, jamais à sa place, et UNIQUEMENT en mode jouable (voir
|
||||||
critique (position:fixed, z-index, display) reste en inline, qui gagne
|
ci-dessus) : tout le positionnement/masquage critique (position:fixed,
|
||||||
toujours sur une règle de classe — si Bulma (chargé depuis un CDN, voir
|
z-index, display) reste en inline, qui gagne toujours sur une règle de
|
||||||
play.html) ne se charge pas (hors-ligne), la boîte de dialogue continue
|
classe — si Bulma (chargé depuis un CDN, voir play.html) ne se charge
|
||||||
de fonctionner exactement pareil, ces classes n'ajoutant qu'un habillage
|
pas (hors-ligne), la boîte de dialogue continue de fonctionner
|
||||||
visuel (ombre, base de police) qui se dégrade sans casser quoi que ce
|
exactement pareil. Pas de ".modal-background" séparé : le voile
|
||||||
soit. Pas de ".modal-background" séparé : le voile semi-transparent est
|
semi-transparent est déjà posé en inline sur cette même balise
|
||||||
déjà posé en inline sur cette même balise (background:rgba(...)) — un
|
(background:rgba(...)) — un second calque tout aussi transparent
|
||||||
second calque tout aussi transparent par-dessus n'ajouterait rien."""
|
par-dessus n'ajouterait rien."""
|
||||||
|
is_play_mode = bool(ctx and ctx.get("_forge_play_mode"))
|
||||||
attrs_raw = el.get("attributes") or {}
|
attrs_raw = el.get("attributes") or {}
|
||||||
box_color = attrs_raw.get("_couleur_boite") or "#1f2430"
|
box_color = attrs_raw.get("_couleur_boite") or "#1f2430"
|
||||||
radius = attrs_raw.get("_arrondi") or "12"
|
radius = attrs_raw.get("_arrondi") or "12"
|
||||||
|
|
||||||
attrs = _visible_attrs(el, meta, ctx)
|
attrs = _visible_attrs(el, meta, ctx)
|
||||||
|
if is_play_mode:
|
||||||
base_overlay_style = (
|
base_overlay_style = (
|
||||||
"position:fixed; inset:0; z-index:9999; display:flex; "
|
"position:fixed; inset:0; z-index:9999; display:flex; "
|
||||||
"align-items:center; justify-content:center; background:rgba(0,0,0,0.6);"
|
"align-items:center; justify-content:center; background:rgba(0,0,0,0.6);"
|
||||||
)
|
)
|
||||||
style = base_overlay_style + " " + _style_string(el, parent_flex_direction=parent_flex_direction)
|
style = base_overlay_style + " " + _style_string(el, parent_flex_direction=parent_flex_direction)
|
||||||
if not (ctx and ctx.get("_forge_play_mode")):
|
wrapper_class = "modal is-active"
|
||||||
# Dans l'ÉDITEUR (ctx sans _forge_play_mode — voir list_elements.py,
|
else:
|
||||||
# posé uniquement en mode JOUABLE), ce widget doit rester visible
|
style = _style_string(el, parent_flex_direction=parent_flex_direction)
|
||||||
# quel que soit son réglage de visibilité statique — "Masqué" par
|
wrapper_class = ""
|
||||||
# défaut à SA création (default_style_for_widget.py), pour ne pas
|
|
||||||
# couvrir tout l'écran dès qu'on le pose. Sans ce forçage, la boîte
|
|
||||||
# de dialogue reste display:none dans le canevas de l'éditeur, donc
|
|
||||||
# invisible dès sa création : impossible d'y voir/positionner son
|
|
||||||
# contenu tant qu'on n'a pas basculé "Visibilité" sur "Visible" à la
|
|
||||||
# main (puis pensé à la remettre sur "Masqué" avant de tester). Le
|
|
||||||
# "display:flex;" ajouté ICI, en dernier dans la chaîne de style,
|
|
||||||
# gagne sur le "display:none" éventuellement posé plus tôt par
|
|
||||||
# _style_string (CSS : même propriété déclarée deux fois -> la
|
|
||||||
# dernière l'emporte). Seul le mode JOUABLE respecte réellement ce
|
|
||||||
# réglage (ou une action "Modifier un élément → Visibilité" qui le
|
|
||||||
# change en cours de partie)."""
|
|
||||||
style += " display:flex;"
|
|
||||||
|
|
||||||
attr_parts = [f'style="{html_lib.escape(style)}"']
|
attr_parts = [f'style="{html_lib.escape(style)}"']
|
||||||
for k, v in attrs.items():
|
for k, v in attrs.items():
|
||||||
@@ -91,8 +92,9 @@ def _render_overlay(el, meta, slug, children_map=None, ctx=None, parent_flex_dir
|
|||||||
"color:#e8eaf0; padding:24px; width:100%; max-width:min(560px, 90%); max-height:90%; overflow:auto; box-sizing:border-box;"
|
"color:#e8eaf0; padding:24px; width:100%; max-width:min(560px, 90%); max-height:90%; overflow:auto; box-sizing:border-box;"
|
||||||
)
|
)
|
||||||
child_html = _render_children(el, slug, children_map, ctx)
|
child_html = _render_children(el, slug, children_map, ctx)
|
||||||
|
wrapper_class_attr = f'class="{wrapper_class}" ' if wrapper_class else ""
|
||||||
return (
|
return (
|
||||||
f'<div class="modal is-active" {" ".join(attr_parts)}>'
|
f'<div {wrapper_class_attr}{" ".join(attr_parts)}>'
|
||||||
f'<div class="box forgeOverlayBox" style="{html_lib.escape(box_style)}">{child_html}</div>'
|
f'<div class="box forgeOverlayBox" style="{html_lib.escape(box_style)}">{child_html}</div>'
|
||||||
f"</div>"
|
f"</div>"
|
||||||
)
|
)
|
||||||
|
|||||||
+35
-18
@@ -203,33 +203,50 @@ def test_overlay_box_default_text_color_survives_the_bulma_box_class(client, gam
|
|||||||
assert "color:#e8eaf0" in box_tag
|
assert "color:#e8eaf0" in box_tag
|
||||||
|
|
||||||
|
|
||||||
def test_overlay_stays_visible_in_the_editor_despite_starting_masked(client, game):
|
def test_overlay_behaves_like_a_normal_container_in_the_editor(client, game):
|
||||||
"""Régression : une "superposition" démarre MASQUÉE par défaut à sa
|
"""Régression (deux retours utilisateur successifs) :
|
||||||
création (display:none — voir default_style_for_widget.py, pour ne
|
1. Un essai précédent forçait "display:flex" dans l'éditeur, quel que
|
||||||
pas couvrir tout l'écran dès qu'on la pose). Ce display:none est écrit
|
soit le réglage "Visibilité" -> "je veux avoir la main sur la
|
||||||
tel quel dans le HTML, aussi bien en mode jouable QUE dans l'éditeur —
|
visibilité de la modale, sinon elle s'affiche toujours sur la
|
||||||
sans forçage, la boîte de dialogue restait donc invisible dans le
|
scène [éditeur] et court-circuite ma logique". L'éditeur doit donc
|
||||||
CANEVAS DE L'ÉDITEUR dès l'instant où elle était créée, rendant
|
respecter "Visibilité" normalement, EXACTEMENT comme n'importe quel
|
||||||
impossible d'y voir/positionner visuellement titres/textes/boutons
|
autre widget (masqué = display:none aussi dans l'éditeur).
|
||||||
posés à l'intérieur ("j'ai mis un texte dedans mais il ne se voit pas"
|
2. La boîte gardait son voile plein écran (position:fixed + fond
|
||||||
— le texte était là, mais toute la boîte qui le contient était
|
assombri) même dans l'éditeur -> "je souhaite que rien ne soit
|
||||||
display:none). Seul le mode JOUABLE doit respecter ce réglage."""
|
assombri, l'assombrissement ne se fait que quand la scène est
|
||||||
|
jouée". Le voile plein écran ne doit donc apparaître qu'en mode
|
||||||
|
JOUABLE — dans l'éditeur, ce widget se comporte comme un conteneur
|
||||||
|
normal (position/taille selon x/y/width/height, pas de voile)."""
|
||||||
screen_id = _create_screen(client, game)
|
screen_id = _create_screen(client, game)
|
||||||
overlay_id = _add_element(client, game, screen_id, "superposition")
|
overlay_id = _add_element(client, game, screen_id, "superposition")
|
||||||
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
|
||||||
|
|
||||||
|
# Par défaut ("Masqué" à la création) : invisible dans l'éditeur aussi,
|
||||||
|
# comme n'importe quel autre widget masqué (pas de forçage).
|
||||||
edit_html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
edit_html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
idx = edit_html.find(f'id="elt-{overlay_id}"')
|
idx = edit_html.find(f'id="elt-{overlay_id}"')
|
||||||
assert idx != -1
|
assert idx != -1
|
||||||
wrapper_style = re.search(r'style="([^"]*)"', edit_html[edit_html.rfind("<div", 0, idx):idx]).group(1)
|
tag = edit_html[edit_html.rfind("<div", 0, idx):idx + 30]
|
||||||
# display:none PUIS display:flex (la dernière déclaration CSS gagne).
|
assert "display:none" in tag
|
||||||
assert wrapper_style.rstrip().endswith("display:flex;")
|
assert "position:fixed" not in tag
|
||||||
|
assert "rgba(0,0,0,0.6)" not in tag
|
||||||
|
|
||||||
|
# Repassé "Visible" à la main (garder la main sur la visibilité) :
|
||||||
|
# apparaît dans l'éditeur, mais toujours SANS voile plein écran.
|
||||||
|
client.post(f"/game/{game}/elements/{overlay_id}/save", data={"ctrl_visibilite": "visible"})
|
||||||
|
edit_html2 = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
|
||||||
|
idx2 = edit_html2.find(f'id="elt-{overlay_id}"')
|
||||||
|
tag2 = edit_html2[edit_html2.rfind("<div", 0, idx2):idx2 + 30]
|
||||||
|
assert "display:none" not in tag2
|
||||||
|
assert "position:fixed" not in tag2
|
||||||
|
assert "rgba(0,0,0,0.6)" not in tag2
|
||||||
|
|
||||||
|
# En mode JOUABLE, le comportement plein écran + voile reste inchangé.
|
||||||
play_html = client.get(f"/game/{game}/play").data.decode()
|
play_html = client.get(f"/game/{game}/play").data.decode()
|
||||||
idx2 = play_html.find(f'id="elt-{overlay_id}"')
|
idx3 = play_html.find(f'id="elt-{overlay_id}"')
|
||||||
wrapper_style2 = re.search(r'style="([^"]*)"', play_html[play_html.rfind("<div", 0, idx2):idx2]).group(1)
|
tag3 = play_html[play_html.rfind("<div", 0, idx3):idx3 + 250]
|
||||||
# En mode jouable, pas de forçage : le display:none par défaut reste actif.
|
assert "position:fixed" in tag3
|
||||||
assert wrapper_style2.rstrip().endswith("display:none;")
|
assert "rgba(0,0,0,0.6)" in tag3
|
||||||
|
|
||||||
|
|
||||||
def test_overlay_element_type_instance_has_no_visible_wrapper_box(client, game):
|
def test_overlay_element_type_instance_has_no_visible_wrapper_box(client, game):
|
||||||
|
|||||||
Reference in New Issue
Block a user