Presets cards/bandeau, cache images, appliqué au sélecteur couple. Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
7.2 KiB
Dart
269 lines
7.2 KiB
Dart
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;
|
||
}
|
||
}
|