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>
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user