So I don’t think any behaviour I’m looking for here is beyond what you’d get with, say, a RoundedRectangleBorder (in fact, on test 4 in the below example script, if I swap out my QuarticleBorder with a RRB directly, the image does in fact clip as expected). Yet getting that behaviour is eluding me.
On test 3, obviously I understand why this currently isn’t working as implemented, but to get the desired behaviour on tests 1, 2 and 5 (when the material this is decorating is sized by its child), this is the sensible way, right? When the widget is being sized by constraints, we don’t even need the margin at all, but as far as I know there’s no way of telling the Border that.
Am I in a rabbit hole of “this isn’t the right way to do this in the first place” here or something? Would appreciate any advice.
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// test 1: text inside a container
Container(
decoration: const ShapeDecoration(
color: Colors.amber,
shape: QuarticleBorder(),
),
child: const Text('█Hello World█',
style: TextStyle(fontSize: 24, color: Colors.blue),
strutStyle: StrutStyle(
fontSize: 24,
forceStrutHeight: true,
)),
),
// result of test 1: Pass. The quarticle shape is correctly applied, though a strutstyle is needed to ensure the text widget itself doesn't add any padding. This is fine though, as the decoration can hardly be expected to know the size of the text.
const SizedBox(height: 36),
// test 2: wrapping a sized container to confirm the inner path is correctly calculated
Container(
decoration: const ShapeDecoration(
color: Colors.amber,
shape: QuarticleBorder(),
),
child: Container(height: 20, width: 20, color: Colors.blue),
),
// result of test 2: Pass. the inner path is correctly calculated.
const SizedBox(height: 36),
// test 3: sizing the shape based on constraints rather than the child
Container(
color: Colors.red,
width: 200,
height: 12,
),
const SizedBox(height: 36),
SizedBox(
width: 200,
height: 200,
child: Container(
decoration: const ShapeDecoration(
color: Colors.amber,
shape: QuarticleBorder(),
),
),
),
// result of test 3: Fail. The margin calculation causes the shape to be larger than the the SizedBox it's in.
const SizedBox(height: 36),
// test 4: does the quarticle shape clip an image?
Container(
width: 384,
height: 216,
decoration: const ShapeDecoration(
image: DecorationImage(
image: AssetImage('assets/test_image.jpg'),
fit: BoxFit.fill,
),
shape: QuarticleBorder(),
),
),
// result of test 4: Fail. The image is not clipped by the shape: we can't even see the quarticle shape.
const SizedBox(height: 36),
// test 5: does the quarticle shape dynamically resize based on the child? (using a TextField to check this)
Container(
width: 200,
decoration: const ShapeDecoration(
color: Colors.amber,
shape: QuarticleBorder(),
),
child: EditableText(
controller: TextEditingController(),
focusNode: FocusNode(),
style: const TextStyle(fontSize: 18, color: Colors.blue),
cursorColor: Colors.blue,
backgroundCursorColor: Colors.blue,
strutStyle: const StrutStyle(
fontSize: 18,
forceStrutHeight: true,
),
selectionColor: Colors.blue.withOpacity(0.5),
),
),
// result of test 5: Pass. The shape correctly resizes based on the child.
],
),
),
);
}
}
class QuarticleBorder extends ShapeBorder {
const QuarticleBorder();
double _calculateMargin(double size) {
return size * ((pow(2, 1 / 4) - 1) / 2).toDouble();
}
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path()..addRect(rect);
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
final margin = _calculateMargin(rect.shortestSide);
final outerRect = Rect.fromLTRB(
rect.left - margin,
rect.top - margin,
rect.right + margin,
rect.bottom + margin,
);
return QuarticlePath(outerRect).getPath();
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
@override
ShapeBorder scale(double t) {
return const QuarticleBorder();
}
}
class QuarticlePath {
final Rect rect;
QuarticlePath(this.rect);
Path getPath() {
double cpDistParam = (4 / 3) * (pow(2, 3 / 4) - 1);
double cornerFrame = min(rect.width, rect.height) / 2;
double cpDist = cpDistParam * cornerFrame;
double offset = rect.width - rect.height;
String offsetDir = offset == 0
? "square"
: offset > 0
? "horizontal"
: "vertical";
offset = offset.abs();
Path path = Path();
Offset lastPoint = Offset(rect.left + cornerFrame, rect.top);
path.moveTo(lastPoint.dx, lastPoint.dy);
void moveToNext(double x, double y, [bool move = false]) {
if (move) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
lastPoint = Offset(x, y);
}
void cubicToNext(Offset cp1, Offset cp2, Offset dest, bool move) {
if (move) {
path.moveTo(dest.dx, dest.dy);
} else {
path.cubicTo(cp1.dx, cp1.dy, cp2.dx, cp2.dy, dest.dx, dest.dy);
}
lastPoint = dest;
}
moveToNext(rect.left + cornerFrame, rect.top, true);
if (offsetDir == "horizontal") {
moveToNext(rect.left + cornerFrame + offset, rect.top);
}
cubicToNext(
Offset(lastPoint.dx + cpDist, rect.top),
Offset(rect.right, rect.top + cornerFrame - cpDist),
Offset(rect.right, rect.top + cornerFrame),
false,
);
if (offsetDir == "vertical") {
moveToNext(rect.right, lastPoint.dy + offset);
}
cubicToNext(
Offset(rect.right, lastPoint.dy + cpDist),
Offset(rect.right - cornerFrame + cpDist, rect.bottom),
Offset(rect.right - cornerFrame, rect.bottom),
false,
);
if (offsetDir == "horizontal") {
moveToNext(rect.left + cornerFrame, rect.bottom);
}
cubicToNext(
Offset(rect.left + cornerFrame - cpDist, rect.bottom),
Offset(rect.left, rect.bottom - cornerFrame + cpDist),
Offset(rect.left, rect.bottom - cornerFrame),
false,
);
if (offsetDir == "vertical") {
moveToNext(rect.left, rect.top + cornerFrame);
}
cubicToNext(
Offset(rect.left, rect.top + cornerFrame - cpDist),
Offset(rect.left + cornerFrame - cpDist, rect.top),
Offset(rect.left + cornerFrame, rect.top),
false,
);
return path;
}
}