Files
petitspas/frontend/lib/widgets/quotidien/nine_slice_asset.dart
T
jmartinandCursor 2c461abc82 refactor(#173): widget NineSliceAsset réutilisable (cartes + bandeau)
Presets cards/bandeau, cache images, appliqué au sélecteur couple.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 17:10:41 +02:00

269 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Paramètres de découpe 9-slice (schéma extensible.png).
class NineSliceSpec {
/// Zone centrale extensible en pixels source (LTRB du centre).
final Rect centerSlice;
/// Largeur logique fixe des coins gauche / droite.
final double cornerWidth;
/// Hauteur logique fixe des coins haut / bas.
final double cornerHeight;
const NineSliceSpec({
required this.centerSlice,
required this.cornerWidth,
required this.cornerHeight,
});
/// Bulles / cartes horizontales `card_*_h.png` (~1024×512).
static const cards = NineSliceSpec(
centerSlice: Rect.fromLTRB(160, 90, 864, 422),
cornerWidth: 56,
cornerHeight: 40,
);
/// Bandeaux couple `bandeau_*.png` (~1024×160).
static const bandeau = NineSliceSpec(
centerSlice: Rect.fromLTRB(140, 32, 884, 128),
cornerWidth: 56,
cornerHeight: 28,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NineSliceSpec &&
centerSlice == other.centerSlice &&
cornerWidth == other.cornerWidth &&
cornerHeight == other.cornerHeight;
@override
int get hashCode => Object.hash(centerSlice, cornerWidth, cornerHeight);
}
/// Fond pastel extensible réutilisable (cartes, bandeau couple, etc.).
///
/// Coins à taille logique fixe ; bords H/V + centre s’étirent.
/// Pas de fichier `.9.png` Android.
class NineSliceAsset extends StatefulWidget {
final String assetPath;
final NineSliceSpec spec;
final Widget? child;
final EdgeInsetsGeometry? padding;
final BoxConstraints? constraints;
final double? width;
final double? height;
const NineSliceAsset({
super.key,
required this.assetPath,
this.spec = NineSliceSpec.cards,
this.child,
this.padding,
this.constraints,
this.width,
this.height,
});
/// Raccourci pour les bulles de la file d’attention.
const NineSliceAsset.card({
super.key,
required this.assetPath,
this.child,
this.padding,
this.constraints,
this.width,
this.height,
}) : spec = NineSliceSpec.cards;
/// Raccourci pour le bandeau choix enfant / nounou (ou parents).
const NineSliceAsset.bandeau({
super.key,
required this.assetPath,
this.child,
this.padding,
this.constraints,
this.width,
this.height,
}) : spec = NineSliceSpec.bandeau;
@override
State<NineSliceAsset> createState() => _NineSliceAssetState();
}
/// Cache partagé pour éviter de recharger le même PNG à chaque bulle.
class _NineSliceImageCache {
static final Map<String, Future<ui.Image>> _futures = {};
static Future<ui.Image> load(String path) {
return _futures.putIfAbsent(path, () async {
final data = await rootBundle.load(path);
final codec = await ui.instantiateImageCodec(
data.buffer.asUint8List(),
);
final frame = await codec.getNextFrame();
return frame.image;
});
}
}
class _NineSliceAssetState extends State<NineSliceAsset> {
ui.Image? _image;
String? _loadedPath;
@override
void initState() {
super.initState();
_load();
}
@override
void didUpdateWidget(covariant NineSliceAsset oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.assetPath != widget.assetPath) {
_load();
}
}
Future<void> _load() async {
final path = widget.assetPath;
final image = await _NineSliceImageCache.load(path);
if (!mounted) return;
setState(() {
_image = image;
_loadedPath = path;
});
}
@override
Widget build(BuildContext context) {
final img = _image;
final ready = img != null && _loadedPath == widget.assetPath;
return SizedBox(
width: widget.width,
height: widget.height,
child: Container(
constraints: widget.constraints,
child: CustomPaint(
painter: ready
? _NineSlicePainter(
image: img!,
spec: widget.spec,
)
: null,
child: Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: widget.child,
),
),
),
);
}
}
class _NineSlicePainter extends CustomPainter {
final ui.Image image;
final NineSliceSpec spec;
_NineSlicePainter({
required this.image,
required this.spec,
});
@override
void paint(Canvas canvas, Size size) {
if (size.width <= 0 || size.height <= 0) return;
final srcW = image.width.toDouble();
final srcH = image.height.toDouble();
final centerSlice = spec.centerSlice;
final srcLeft = centerSlice.left.clamp(0.0, srcW);
final srcTop = centerSlice.top.clamp(0.0, srcH);
final srcRight = centerSlice.right.clamp(0.0, srcW);
final srcBottom = centerSlice.bottom.clamp(0.0, srcH);
final srcLeftW = srcLeft;
final srcTopH = srcTop;
final srcRightW = srcW - srcRight;
final srcBottomH = srcH - srcBottom;
final srcCenterW = (srcRight - srcLeft).clamp(1.0, srcW);
final srcCenterH = (srcBottom - srcTop).clamp(1.0, srcH);
final dstLeftW = spec.cornerWidth.clamp(1.0, size.width / 2);
final dstRightW = spec.cornerWidth.clamp(1.0, size.width / 2);
final dstTopH = spec.cornerHeight.clamp(1.0, size.height / 2);
final dstBottomH = spec.cornerHeight.clamp(1.0, size.height / 2);
final dstCenterW =
(size.width - dstLeftW - dstRightW).clamp(0.0, size.width);
final dstCenterH =
(size.height - dstTopH - dstBottomH).clamp(0.0, size.height);
final paint = Paint()..filterQuality = FilterQuality.high;
void draw(Rect src, Rect dst) {
if (src.width <= 0 || src.height <= 0) return;
if (dst.width <= 0 || dst.height <= 0) return;
canvas.drawImageRect(image, src, dst, paint);
}
// Haut
draw(
Rect.fromLTWH(0, 0, srcLeftW, srcTopH),
Rect.fromLTWH(0, 0, dstLeftW, dstTopH),
);
draw(
Rect.fromLTWH(srcLeft, 0, srcCenterW, srcTopH),
Rect.fromLTWH(dstLeftW, 0, dstCenterW, dstTopH),
);
draw(
Rect.fromLTWH(srcRight, 0, srcRightW, srcTopH),
Rect.fromLTWH(dstLeftW + dstCenterW, 0, dstRightW, dstTopH),
);
// Milieu
draw(
Rect.fromLTWH(0, srcTop, srcLeftW, srcCenterH),
Rect.fromLTWH(0, dstTopH, dstLeftW, dstCenterH),
);
draw(
Rect.fromLTWH(srcLeft, srcTop, srcCenterW, srcCenterH),
Rect.fromLTWH(dstLeftW, dstTopH, dstCenterW, dstCenterH),
);
draw(
Rect.fromLTWH(srcRight, srcTop, srcRightW, srcCenterH),
Rect.fromLTWH(dstLeftW + dstCenterW, dstTopH, dstRightW, dstCenterH),
);
// Bas
draw(
Rect.fromLTWH(0, srcBottom, srcLeftW, srcBottomH),
Rect.fromLTWH(0, dstTopH + dstCenterH, dstLeftW, dstBottomH),
);
draw(
Rect.fromLTWH(srcLeft, srcBottom, srcCenterW, srcBottomH),
Rect.fromLTWH(dstLeftW, dstTopH + dstCenterH, dstCenterW, dstBottomH),
);
draw(
Rect.fromLTWH(srcRight, srcBottom, srcRightW, srcBottomH),
Rect.fromLTWH(
dstLeftW + dstCenterW,
dstTopH + dstCenterH,
dstRightW,
dstBottomH,
),
);
}
@override
bool shouldRepaint(covariant _NineSlicePainter oldDelegate) {
return oldDelegate.image != image || oldDelegate.spec != spec;
}
}