Last active
November 19, 2025 14:43
-
-
Save PlugFox/73fdd4f3d366a8f3e8662768226af36f to your computer and use it in GitHub Desktop.
Superellipse Input Border for Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Superellipse Input Border for Flutter | |
| * https://gist.github.com/PlugFox/73fdd4f3d366a8f3e8662768226af36f | |
| * https://dartpad.dev?id=73fdd4f3d366a8f3e8662768226af36f | |
| * Mike Matiunin <[email protected]>, 19 November 2025 | |
| */ | |
| import 'package:flutter/material.dart'; | |
| void main() => runApp( | |
| const MaterialApp( | |
| home: Scaffold( | |
| body: SafeArea( | |
| child: Center( | |
| child: DecoratedText(), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| class DecoratedText extends StatelessWidget { | |
| const DecoratedText({ | |
| super.key, | |
| }); | |
| @override | |
| Widget build(BuildContext context) => SizedBox( | |
| width: 128, | |
| height: 64, | |
| child: InputDecorator( | |
| decoration: InputDecoration( | |
| border: ShapeInputBorder.superellipse( | |
| borderRadius: const BorderRadius.all(Radius.circular(24)), | |
| side: const BorderSide(color: Colors.blue, width: 2), | |
| ), | |
| ), | |
| child: const Text('Hello, Superellipse!'), | |
| ), | |
| ); | |
| } | |
| class ShapeInputBorder extends InputBorder { | |
| const ShapeInputBorder({ | |
| required this.shape, | |
| super.borderSide, | |
| }); | |
| ShapeInputBorder.superellipse({ | |
| BorderSide side = BorderSide.none, | |
| BorderRadiusGeometry borderRadius = BorderRadius.zero, | |
| }) : this( | |
| shape: RoundedSuperellipseBorder( | |
| side: side, | |
| borderRadius: borderRadius, | |
| ), | |
| borderSide: side, | |
| ); | |
| final ShapeBorder shape; | |
| @override | |
| bool get isOutline => true; | |
| @override | |
| ShapeInputBorder copyWith({BorderSide? borderSide, OutlinedBorder? shape}) => ShapeInputBorder( | |
| shape: shape ?? this.shape, | |
| borderSide: borderSide ?? this.borderSide, | |
| ); | |
| @override | |
| EdgeInsetsGeometry get dimensions => EdgeInsets.all(borderSide.width); | |
| @override | |
| Path getInnerPath(Rect rect, {TextDirection? textDirection}) => | |
| shape.getInnerPath(rect, textDirection: textDirection); | |
| @override | |
| Path getOuterPath(Rect rect, {TextDirection? textDirection}) => | |
| shape.getOuterPath(rect, textDirection: textDirection); | |
| @override | |
| void paint( | |
| Canvas canvas, | |
| Rect rect, { | |
| double? gapStart, | |
| double? gapExtent = 0.0, | |
| double gapPercentage = 0.0, | |
| TextDirection? textDirection, | |
| }) { | |
| final paint = borderSide.toPaint(); | |
| final outerPath = shape.getOuterPath(rect, textDirection: textDirection); | |
| if (gapExtent == null || gapExtent <= 0.0 || gapPercentage == 0.0) { | |
| canvas.drawPath(outerPath, paint); | |
| return; | |
| } | |
| final extent = gapExtent + 5; | |
| final start = gapStart ?? 2 - 2; | |
| final gapRect = Rect.fromLTWH( | |
| textDirection == TextDirection.rtl ? rect.width - start - extent : start, | |
| rect.top - borderSide.width, | |
| extent, | |
| borderSide.width * 2, | |
| ); | |
| final gapPath = Path()..addRect(gapRect); | |
| final resultPath = Path.combine( | |
| PathOperation.difference, | |
| outerPath, | |
| gapPath, | |
| ); | |
| canvas.drawPath(resultPath, paint); | |
| } | |
| @override | |
| ShapeInputBorder scale(double t) => ShapeInputBorder( | |
| shape: shape.scale(t), | |
| borderSide: borderSide.scale(t), | |
| ); | |
| } | |
| class RoundedSuperellipseInputBorder extends RoundedSuperellipseBorder implements InputBorder { | |
| const RoundedSuperellipseInputBorder({ | |
| super.side = BorderSide.none, | |
| super.borderRadius = BorderRadius.zero, | |
| }); | |
| @override | |
| BorderSide get borderSide => super.side; | |
| @override | |
| bool get isOutline => true; | |
| RoundedSuperellipseInputBorder copyWith({ | |
| BorderSide? side, | |
| BorderSide? borderSide, | |
| BorderRadiusGeometry? borderRadius, | |
| }) { | |
| assert( | |
| side == null || borderSide == null, | |
| 'Cannot provide both side and borderSide', | |
| ); | |
| return RoundedSuperellipseInputBorder( | |
| side: side ?? borderSide ?? this.side, | |
| borderRadius: borderRadius ?? this.borderRadius, | |
| ); | |
| } | |
| @override | |
| void paint( | |
| Canvas canvas, | |
| Rect rect, { | |
| double? gapStart, | |
| double? gapExtent = 0.0, | |
| double gapPercentage = 0.0, | |
| TextDirection? textDirection, | |
| }) { | |
| final paint = borderSide.toPaint(); | |
| final outerPath = super.getOuterPath(rect, textDirection: textDirection); | |
| if (gapExtent == null || gapExtent <= 0.0 || gapPercentage == 0.0) { | |
| canvas.drawPath(outerPath, paint); | |
| return; | |
| } | |
| final extent = gapExtent + 5; | |
| final start = gapStart ?? 2 - 2; | |
| final gapRect = Rect.fromLTWH( | |
| textDirection == TextDirection.rtl ? rect.width - start - extent : start, | |
| rect.top - borderSide.width, | |
| extent, | |
| borderSide.width * 2, | |
| ); | |
| final gapPath = Path()..addRect(gapRect); | |
| final resultPath = Path.combine( | |
| PathOperation.difference, | |
| outerPath, | |
| gapPath, | |
| ); | |
| canvas.drawPath(resultPath, paint); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment