Dev #1

Merged
w-vandal merged 62 commits from dev into main 2026-08-26 04:56:44 +00:00
7 changed files with 151 additions and 1 deletions
Showing only changes of commit bda082bffd - Show all commits
+41
View File
@@ -6,6 +6,16 @@ from ..widgets.widget_meta import widget_meta
from .get_element import get_element
def _hex_to_rgb(hex_color):
h = (hex_color or "").lstrip("#")
if len(h) != 6:
return (0, 0, 0)
try:
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
except ValueError:
return (0, 0, 0)
def _is_default_value(control, raw_value):
"""Compare la valeur reçue du formulaire à la valeur par défaut du
réglage (en tenant compte de l'unité ajoutée pour les sliders), pour
@@ -83,6 +93,37 @@ def save_element_controls(slug, element_id, form):
style[name] = f"{num_str}{unit}"
continue
if ctype == "shadow":
# 6 entrées de formulaire (décalage X/Y, flou, étendue, couleur,
# opacité — voir c_shadow.py) combinées en une seule valeur CSS
# "box-shadow" (couleur+opacité fusionnées en rgba(), un
# <input type="color"> seul ne portant pas de canal alpha).
_kind, name = control["target"].split(":", 1)
defaults = control.get("default", {})
def _num(suffix, fallback):
try:
return float(form.get(field_name + suffix, fallback))
except (TypeError, ValueError):
return fallback
x = _num("_x", defaults.get("x", 0))
y = _num("_y", defaults.get("y", 4))
blur = _num("_blur", defaults.get("blur", 12))
spread = _num("_spread", defaults.get("spread", 0))
opacity = _num("_opacity", defaults.get("opacity", 0.4))
color = form.get(field_name + "_color") or defaults.get("color", "#000000")
if x == 0 and y == 0 and blur == 0 and spread == 0:
style.pop(name, None)
else:
r, g, b = _hex_to_rgb(color)
def _px(n):
return str(int(n)) if n == int(n) else str(n)
style[name] = f"{_px(x)}px {_px(y)}px {_px(blur)}px {_px(spread)}px rgba({r}, {g}, {b}, {opacity})"
continue
target = control["target"]
if ctype == "checkbox":
checked = form.get(field_name) is not None
@@ -0,0 +1,5 @@
from ..controls.c_shadow import c_shadow
SHADOW_CONTROLS = [
c_shadow("box-shadow", "Ombre portée", "style:box-shadow", group="Ombre"),
]
@@ -4,6 +4,7 @@ from .visibility_condition_controls import VISIBILITY_CONDITION_CONTROLS
from .size_override_controls import SIZE_OVERRIDE_CONTROLS
from .spacing_controls import SPACING_CONTROLS
from .border_controls import BORDER_CONTROLS
from .shadow_controls import SHADOW_CONTROLS
# Le panneau "Survol" (HOVER_CONTROLS) a été retiré du panneau de propriétés :
# survoler un élément est conceptuellement un DÉCLENCHEUR (voir la Logique de
@@ -12,4 +13,4 @@ from .border_controls import BORDER_CONTROLS
# et bindHoverTexts() dans play.html) reste fonctionnel pour les éléments qui
# en avaient déjà un, seul le moyen de le RÉGLER depuis ce panneau disparaît
# pour l'instant.
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + VISIBILITY_CONDITION_CONTROLS
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + SHADOW_CONTROLS + VISIBILITY_CONDITION_CONTROLS
+15
View File
@@ -31,6 +31,21 @@ def _control_value(el, control):
return {"value": float(m.group(1)), "unit": m.group(2)}
return {"value": control.get("default", 0), "unit": "%"}
if ctype == "shadow":
_kind, name = control["target"].split(":", 1)
raw = style.get(name)
m = re.match(
r"^(-?\d+)px (-?\d+)px (\d+)px (\d+)px rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$",
str(raw or ""),
)
if m:
x, y, blur, spread, r, g, b, a = m.groups()
return {
"x": int(x), "y": int(y), "blur": int(blur), "spread": int(spread),
"color": "#{:02x}{:02x}{:02x}".format(int(r), int(g), int(b)), "opacity": float(a),
}
return control.get("default", {})
target = control["target"]
if target == "content":
return el.get("content") or control.get("default", "")
+12
View File
@@ -0,0 +1,12 @@
def c_shadow(key, label, target, default=None, group=None):
"""Ombre portée composée de 6 sous-réglages (décalage X/Y, flou,
étendue, couleur, opacité) — voir save_element_controls.py (ctype
"shadow") pour comment ils se combinent en une seule valeur CSS
"box-shadow", et control_value.py pour comment elle se décompose à
nouveau pour l'affichage. Décalages/flou/étendue tous à 0 = pas
d'ombre, comme border-width à 0 = pas de bordure."""
return {
"key": key, "label": label, "type": "shadow", "target": target,
"default": default or {"x": 0, "y": 4, "blur": 12, "spread": 0, "color": "#000000", "opacity": 0.4},
"group": group,
}
+20
View File
@@ -730,6 +730,26 @@
oninput="this.nextElementSibling.textContent = 'x' + this.value">
<span class="sliderValue tag">x{{ c.value }}</span>
</div>
{% elif c.type == 'shadow' %}
<div class="control">
<div class="fieldRow">
<div class="formField" style="flex:1;"><label class="label is-small">Décalage X</label><input class="input is-small" type="number" name="ctrl_{{ c.key }}_x" value="{{ c.value.x }}"></div>
<div class="formField" style="flex:1;"><label class="label is-small">Décalage Y</label><input class="input is-small" type="number" name="ctrl_{{ c.key }}_y" value="{{ c.value.y }}"></div>
</div>
<div class="fieldRow" style="margin-top:6px;">
<div class="formField" style="flex:1;"><label class="label is-small">Flou</label><input class="input is-small" type="number" min="0" name="ctrl_{{ c.key }}_blur" value="{{ c.value.blur }}"></div>
<div class="formField" style="flex:1;"><label class="label is-small">Étendue</label><input class="input is-small" type="number" min="0" name="ctrl_{{ c.key }}_spread" value="{{ c.value.spread }}"></div>
</div>
<div class="fieldRow" style="margin-top:6px; align-items:center;">
<input class="input" type="color" style="flex:0 0 60px; padding:2px;" name="ctrl_{{ c.key }}_color" value="{{ c.value.color }}">
<div class="sliderRow" style="flex:1;">
<input type="range" name="ctrl_{{ c.key }}_opacity" min="0" max="1" step="0.05" value="{{ c.value.opacity }}"
oninput="this.nextElementSibling.textContent = Math.round(this.value * 100) + '%'">
<span class="sliderValue tag">{{ (c.value.opacity * 100)|round|int }}%</span>
</div>
</div>
<p class="help">Décalages, flou et étendue tous à 0 = pas d'ombre.</p>
</div>
{% elif c.type == 'align' %}
<div class="control">
<div class="buttons has-addons alignGroup">
+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"]