Files
petitspas/frontend/lib/widgets/quotidien/carte.dart
T
jmartinandCursor 5eb0f13e75 refactor(#173): widget Carte (extensible par défaut) + fix bandeau
Renomme NineSlice* en Carte. Le bandeau couple s’étire en
horizontal seulement pour garder les bouts demi-cercle.

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

252 lines
7.4 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';
/// Mode d’extension du fond pastel.
enum CarteStretch {
/// 9 zones (bulles / cartes content).
both,
/// Uniquement horizontal — bouts en demi-cercle (bandeau couple).
horizontal,
}
/// Fond pastel extensible (capacité par défaut des « cartes » UI).
///
/// Coins / bouts à taille logique fixe ; le reste s’étire selon [stretch].
class Carte extends StatefulWidget {
final String assetPath;
final CarteStretch stretch;
final Rect centerSlice;
final double cornerWidth;
final double cornerHeight;
final Widget? child;
final EdgeInsetsGeometry? padding;
final BoxConstraints? constraints;
final double? width;
final double? height;
/// Carte / bulle content (`card_*_h.png`, ~1024×512).
const Carte({
super.key,
required this.assetPath,
this.child,
this.padding,
this.constraints,
this.width,
this.height,
this.stretch = CarteStretch.both,
this.centerSlice = const Rect.fromLTRB(160, 90, 864, 422),
this.cornerWidth = 56,
this.cornerHeight = 40,
});
/// Bandeau couple enfant / nounou (`bandeau_*.png`, ~1024×160).
/// Étirement horizontal seul pour garder les bouts ronds.
const Carte.bandeau({
super.key,
required this.assetPath,
this.child,
this.padding,
this.constraints,
this.width,
this.height,
}) : stretch = CarteStretch.horizontal,
// Demi-cercle source ≈ moitié de la hauteur (160/2 = 80).
centerSlice = const Rect.fromLTRB(80, 0, 944, 160),
cornerWidth = 45,
cornerHeight = 45;
@override
State<Carte> createState() => _CarteState();
}
class _CarteImageCache {
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 _CarteState extends State<Carte> {
ui.Image? _image;
String? _loadedPath;
@override
void initState() {
super.initState();
_load();
}
@override
void didUpdateWidget(covariant Carte oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.assetPath != widget.assetPath) {
_load();
}
}
Future<void> _load() async {
final path = widget.assetPath;
final image = await _CarteImageCache.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
? _CartePainter(
image: img!,
stretch: widget.stretch,
centerSlice: widget.centerSlice,
cornerWidth: widget.cornerWidth,
cornerHeight: widget.cornerHeight,
)
: null,
child: Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: widget.child,
),
),
),
);
}
}
class _CartePainter extends CustomPainter {
final ui.Image image;
final CarteStretch stretch;
final Rect centerSlice;
final double cornerWidth;
final double cornerHeight;
_CartePainter({
required this.image,
required this.stretch,
required this.centerSlice,
required this.cornerWidth,
required this.cornerHeight,
});
@override
void paint(Canvas canvas, Size size) {
if (size.width <= 0 || size.height <= 0) return;
final paint = Paint()..filterQuality = FilterQuality.high;
final srcW = image.width.toDouble();
final srcH = image.height.toDouble();
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);
}
if (stretch == CarteStretch.horizontal) {
// 3 bandes pleine hauteur : bout | milieu | bout (pilule).
// Largeur du bout = moitié de la hauteur → demi-cercle propre.
final tip = (size.height / 2).clamp(1.0, size.width / 2);
final srcTip = centerSlice.left.clamp(1.0, srcW / 2);
final midW = (size.width - tip * 2).clamp(0.0, size.width);
draw(
Rect.fromLTWH(0, 0, srcTip, srcH),
Rect.fromLTWH(0, 0, tip, size.height),
);
draw(
Rect.fromLTWH(srcTip, 0, (srcW - srcTip * 2).clamp(1.0, srcW), srcH),
Rect.fromLTWH(tip, 0, midW, size.height),
);
draw(
Rect.fromLTWH(srcW - srcTip, 0, srcTip, srcH),
Rect.fromLTWH(tip + midW, 0, tip, size.height),
);
return;
}
// 9 zones (bulles).
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 = cornerWidth.clamp(1.0, size.width / 2);
final dstRightW = cornerWidth.clamp(1.0, size.width / 2);
final dstTopH = cornerHeight.clamp(1.0, size.height / 2);
final dstBottomH = 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);
// 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 _CartePainter oldDelegate) {
return oldDelegate.image != image ||
oldDelegate.stretch != stretch ||
oldDelegate.centerSlice != centerSlice ||
oldDelegate.cornerWidth != cornerWidth ||
oldDelegate.cornerHeight != cornerHeight;
}
}