Ajoute une propriété "Ombre portée" (box-shadow), disponible sur tout widget

Nouveau groupe "Ombre" dans le panneau de propriétés, à côté de "Bordure"
(même universalité — voir UNIVERSAL_CONTROLS) : décalage X/Y, flou,
étendue, couleur et opacité, combinés en une seule valeur CSS box-shadow
(couleur+opacité fusionnées en rgba(), un <input type="color"> seul ne
portant pas de canal alpha). Décalages/flou/étendue tous à 0 = pas
d'ombre, même convention que border-width à 0 = pas de bordure.

Nouveau ctype "shadow" (c_shadow.py), suit exactement le même principe
que le ctype "size" déjà existant (size_override_controls.py) : plusieurs
entrées de formulaire pour un seul réglage, composées/décomposées dans
save_element_controls.py et control_value.py plutôt que passées par le
chemin générique clé->valeur.

Ajoute tests/test_shadow_controls.py (présence dans le panneau,
composition de la valeur CSS, aller-retour dans le formulaire, remise à
zéro qui retire l'ombre).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-25 14:37:38 +02:00
co-authored by Claude Sonnet 5
parent 9c9e3c5379
commit bda082bffd
7 changed files with 151 additions and 1 deletions
+56
View File
@@ -0,0 +1,56 @@
"""Ombre portée (panneau de propriétés, tout widget) : 6 sous-réglages
(décalage X/Y, flou, étendue, couleur, opacité) combinés en une seule
valeur CSS box-shadow."""
import re
import screens
def _create_element(client, slug, widget="conteneur"):
resp = client.post(f"/game/{slug}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False)
element_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
return screen_id, element_id
def test_shadow_controls_appear_in_the_properties_panel(client, game):
screen_id, element_id = _create_element(client, game)
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={element_id}").data.decode()
assert "ctrl_box-shadow_x" in html
assert "ctrl_box-shadow_color" in html
def test_saving_a_shadow_composes_box_shadow_css(client, game):
screen_id, element_id = _create_element(client, game)
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_box-shadow_x": "2", "ctrl_box-shadow_y": "6", "ctrl_box-shadow_blur": "20",
"ctrl_box-shadow_spread": "3", "ctrl_box-shadow_color": "#ff0000", "ctrl_box-shadow_opacity": "0.5",
})
el = screens.get_element(game, element_id)
assert el["style"]["box-shadow"] == "2px 6px 20px 3px rgba(255, 0, 0, 0.5)"
def test_shadow_value_round_trips_back_into_the_form(client, game):
screen_id, element_id = _create_element(client, game)
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_box-shadow_x": "2", "ctrl_box-shadow_y": "6", "ctrl_box-shadow_blur": "20",
"ctrl_box-shadow_spread": "3", "ctrl_box-shadow_color": "#ff0000", "ctrl_box-shadow_opacity": "0.5",
})
html = client.get(f"/game/{game}/screens/{screen_id}/edit?selected={element_id}").data.decode()
assert 'name="ctrl_box-shadow_x" value="2.0"' in html or 'name="ctrl_box-shadow_x" value="2"' in html
assert 'name="ctrl_box-shadow_color" value="#ff0000"' in html
def test_all_zero_offsets_remove_the_shadow(client, game):
screen_id, element_id = _create_element(client, game)
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_box-shadow_x": "2", "ctrl_box-shadow_y": "6", "ctrl_box-shadow_blur": "20",
"ctrl_box-shadow_spread": "3", "ctrl_box-shadow_color": "#ff0000", "ctrl_box-shadow_opacity": "0.5",
})
client.post(f"/game/{game}/elements/{element_id}/save", data={
"ctrl_box-shadow_x": "0", "ctrl_box-shadow_y": "0", "ctrl_box-shadow_blur": "0",
"ctrl_box-shadow_spread": "0", "ctrl_box-shadow_color": "#ff0000", "ctrl_box-shadow_opacity": "0.5",
})
el = screens.get_element(game, element_id)
assert "box-shadow" not in el["style"]