Milad akarie
Committed by David PHAM-VAN

Add RTL support to BorderRadius widget

Add RTL support Alignment
... ... @@ -16,15 +16,10 @@
import 'dart:math' as math;
import 'package:pdf/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
import '../../pdf.dart';
import 'box_border.dart';
import 'container.dart';
import 'decoration.dart';
import 'geometry.dart';
import 'widget.dart';
import '../../widgets.dart';
enum BoxFit { fill, contain, cover, fitWidth, fitHeight, none, scaleDown }
... ... @@ -303,7 +298,7 @@ class Align extends SingleChildWidget {
super(child: child);
/// How to align the child.
final Alignment alignment;
final AlignmentGeometry alignment;
/// If non-null, sets its width to the child's width multiplied by this factor.
final double? widthFactor;
... ... @@ -330,8 +325,8 @@ class Align extends SingleChildWidget {
height: shrinkWrapHeight
? child!.box!.height * (heightFactor ?? 1.0)
: double.infinity);
child!.box = alignment.inscribe(child!.box!.size, box!);
final resolvedAlignment = alignment.resolve(Directionality.of(context));
child!.box = resolvedAlignment.inscribe(child!.box!.size, box!);
} else {
box = constraints.constrainRect(
width: shrinkWrapWidth ? 0.0 : double.infinity,
... ...
... ... @@ -14,8 +14,10 @@
* limitations under the License.
*/
import 'package:meta/meta.dart';
import '../../pdf.dart';
import 'widget.dart';
import '../../widgets.dart';
/// A radius for either circular or elliptical shapes.
class Radius {
... ... @@ -35,8 +37,149 @@ class Radius {
static const Radius zero = Radius.circular(0.0);
}
/// Base class for [BorderRadius] that allows for text-direction aware resolution.
///
/// A property or argument of this type accepts classes created either with [
/// BorderRadius.only] and its variants, or [BorderRadiusDirectional.only]
/// and its variants.
///
/// To convert a [BorderRadiusGeometry] object of indeterminate type into a
/// [BorderRadius] object, call the [resolve] method.
@immutable
abstract class BorderRadiusGeometry {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const BorderRadiusGeometry();
Radius get _topLeft;
Radius get _topRight;
Radius get _bottomLeft;
Radius get _bottomRight;
Radius get _topStart;
Radius get _topEnd;
Radius get _bottomStart;
Radius get _bottomEnd;
Radius get uniform;
bool get isUniform;
/// Convert this instance into a [BorderRadius], so that the radii are
/// expressed for specific physical corners (top-left, top-right, etc) rather
/// than in a direction-dependent manner.
///
/// See also:
///
/// * [BorderRadius], for which this is a no-op (returns itself).
/// * [BorderRadiusDirectional], which flips the horizontal direction
/// based on the `direction` argument.
BorderRadius resolve(TextDirection? direction);
@override
String toString() {
String? visual, logical;
if (_topLeft == _topRight && _topRight == _bottomLeft && _bottomLeft == _bottomRight) {
if (_topLeft != Radius.zero) {
if (_topLeft.x == _topLeft.y) {
visual = 'BorderRadius.circular(${_topLeft.x.toStringAsFixed(1)})';
} else {
visual = 'BorderRadius.all($_topLeft)';
}
}
} else {
// visuals aren't the same and at least one isn't zero
final result = StringBuffer();
result.write('BorderRadius.only(');
var comma = false;
if (_topLeft != Radius.zero) {
result.write('topLeft: $_topLeft');
comma = true;
}
if (_topRight != Radius.zero) {
if (comma) {
result.write(', ');
}
result.write('topRight: $_topRight');
comma = true;
}
if (_bottomLeft != Radius.zero) {
if (comma) {
result.write(', ');
}
result.write('bottomLeft: $_bottomLeft');
comma = true;
}
if (_bottomRight != Radius.zero) {
if (comma) {
result.write(', ');
}
result.write('bottomRight: $_bottomRight');
}
result.write(')');
visual = result.toString();
}
if (_topStart == _topEnd && _topEnd == _bottomEnd && _bottomEnd == _bottomStart) {
if (_topStart != Radius.zero) {
if (_topStart.x == _topStart.y) {
logical = 'BorderRadiusDirectional.circular(${_topStart.x.toStringAsFixed(1)})';
} else {
logical = 'BorderRadiusDirectional.all($_topStart)';
}
}
} else {
// logical aren't the same and at least one isn't zero
final result = StringBuffer();
result.write('BorderRadiusDirectional.only(');
var comma = false;
if (_topStart != Radius.zero) {
result.write('topStart: $_topStart');
comma = true;
}
if (_topEnd != Radius.zero) {
if (comma) {
result.write(', ');
}
result.write('topEnd: $_topEnd');
comma = true;
}
if (_bottomStart != Radius.zero) {
if (comma) {
result.write(', ');
}
result.write('bottomStart: $_bottomStart');
comma = true;
}
if (_bottomEnd != Radius.zero) {
if (comma) {
result.write(', ');
}
result.write('bottomEnd: $_bottomEnd');
}
result.write(')');
logical = result.toString();
}
if (visual != null && logical != null) {
return '$visual + $logical';
}
if (visual != null) {
return visual;
}
if (logical != null) {
return logical;
}
return 'BorderRadius.zero';
}
}
/// An immutable set of radii for each corner of a rectangle.
class BorderRadius {
class BorderRadius extends BorderRadiusGeometry {
/// Creates a border radius where all radii are [radius].
const BorderRadius.all(Radius radius)
: this.only(
... ... @@ -85,6 +228,8 @@ class BorderRadius {
this.bottomRight = Radius.zero,
});
/// A border radius with all zero radii.
static const BorderRadius zero = BorderRadius.all(Radius.zero);
... ... @@ -100,6 +245,14 @@ class BorderRadius {
/// The bottom-right [Radius].
final Radius bottomRight;
@override
bool get isUniform => topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight;
@override
Radius get uniform => isUniform ? topLeft : Radius.zero;
void paint(Context context, PdfRect box) {
// Ellipse 4-spline magic number
const _m4 = 0.551784;
... ... @@ -108,23 +261,13 @@ class BorderRadius {
// Start
..moveTo(box.x, box.y + bottomLeft.y)
// bottomLeft
..curveTo(
box.x,
box.y - _m4 * bottomLeft.y + bottomLeft.y,
box.x - _m4 * bottomLeft.x + bottomLeft.x,
box.y,
box.x + bottomLeft.x,
box.y)
..curveTo(box.x, box.y - _m4 * bottomLeft.y + bottomLeft.y, box.x - _m4 * bottomLeft.x + bottomLeft.x, box.y,
box.x + bottomLeft.x, box.y)
// bottom
..lineTo(box.x + box.width - bottomRight.x, box.y)
// bottomRight
..curveTo(
box.x + _m4 * bottomRight.x + box.width - bottomRight.x,
box.y,
box.x + box.width,
box.y - _m4 * bottomRight.y + bottomRight.y,
box.x + box.width,
box.y + bottomRight.y)
..curveTo(box.x + _m4 * bottomRight.x + box.width - bottomRight.x, box.y, box.x + box.width,
box.y - _m4 * bottomRight.y + bottomRight.y, box.x + box.width, box.y + bottomRight.y)
// right
..lineTo(box.x + box.width, box.y + box.height - topRight.y)
// topRight
... ... @@ -138,14 +281,165 @@ class BorderRadius {
// top
..lineTo(box.x + topLeft.x, box.y + box.height)
// topLeft
..curveTo(
box.x - _m4 * topLeft.x + topLeft.x,
box.y + box.height,
box.x,
box.y + _m4 * topLeft.y + box.height - topLeft.y,
box.x,
box.y + box.height - topLeft.y)
..curveTo(box.x - _m4 * topLeft.x + topLeft.x, box.y + box.height, box.x,
box.y + _m4 * topLeft.y + box.height - topLeft.y, box.x, box.y + box.height - topLeft.y)
// left
..lineTo(box.x, box.y + bottomLeft.y);
}
@override
Radius get _topLeft => topLeft;
@override
Radius get _topRight => topRight;
@override
Radius get _bottomLeft => bottomLeft;
@override
Radius get _bottomRight => bottomRight;
@override
Radius get _topStart => Radius.zero;
@override
Radius get _topEnd => Radius.zero;
@override
Radius get _bottomStart => Radius.zero;
@override
Radius get _bottomEnd => Radius.zero;
@override
BorderRadius resolve(TextDirection? direction) => this;
}
/// An immutable set of radii for each corner of a rectangle, but with the
/// corners specified in a manner dependent on the writing direction.
///
/// This can be used to specify a corner radius on the leading or trailing edge
/// of a box, so that it flips to the other side when the text alignment flips
/// (e.g. being on the top right in English text but the top left in Arabic
/// text).
///
/// See also:
///
/// * [BorderRadius], a variant that uses physical labels (`topLeft` and
/// `topRight` instead of `topStart` and `topEnd`).
class BorderRadiusDirectional extends BorderRadiusGeometry {
/// Creates a border radius where all radii are [radius].
const BorderRadiusDirectional.all(Radius radius) : this.only(
topStart: radius,
topEnd: radius,
bottomStart: radius,
bottomEnd: radius,
);
/// Creates a border radius where all radii are [Radius.circular(radius)].
BorderRadiusDirectional.circular(double radius) : this.all(
Radius.circular(radius),
);
/// Creates a vertically symmetric border radius where the top and bottom
/// sides of the rectangle have the same radii.
const BorderRadiusDirectional.vertical({
Radius top = Radius.zero,
Radius bottom = Radius.zero,
}) : this.only(
topStart: top,
topEnd: top,
bottomStart: bottom,
bottomEnd: bottom,
);
/// Creates a horizontally symmetrical border radius where the start and end
/// sides of the rectangle have the same radii.
const BorderRadiusDirectional.horizontal({
Radius start = Radius.zero,
Radius end = Radius.zero,
}) : this.only(
topStart: start,
topEnd: end,
bottomStart: start,
bottomEnd: end,
);
/// Creates a border radius with only the given non-zero values. The other
/// corners will be right angles.
const BorderRadiusDirectional.only({
this.topStart = Radius.zero,
this.topEnd = Radius.zero,
this.bottomStart = Radius.zero,
this.bottomEnd = Radius.zero,
});
/// A border radius with all zero radii.
///
/// Consider using [BorderRadius.zero] instead, since that object has the same
/// effect, but will be cheaper to [resolve].
static const BorderRadiusDirectional zero = BorderRadiusDirectional.all(Radius.zero);
/// The top-start [Radius].
final Radius topStart;
@override
Radius get _topStart => topStart;
/// The top-end [Radius].
final Radius topEnd;
@override
Radius get _topEnd => topEnd;
/// The bottom-start [Radius].
final Radius bottomStart;
@override
Radius get _bottomStart => bottomStart;
/// The bottom-end [Radius].
final Radius bottomEnd;
@override
Radius get _bottomEnd => bottomEnd;
@override
Radius get _topLeft => Radius.zero;
@override
Radius get _topRight => Radius.zero;
@override
Radius get _bottomLeft => Radius.zero;
@override
Radius get _bottomRight => Radius.zero;
@override
bool get isUniform => topStart == topEnd && topStart == bottomStart && topStart == bottomEnd;
@override
Radius get uniform => isUniform ? topStart : Radius.zero;
@override
BorderRadius resolve(TextDirection? direction) {
assert(direction != null);
switch (direction!) {
case TextDirection.rtl:
return BorderRadius.only(
topLeft: topEnd,
topRight: topStart,
bottomLeft: bottomEnd,
bottomRight: bottomStart,
);
case TextDirection.ltr:
return BorderRadius.only(
topLeft: topStart,
topRight: topEnd,
bottomLeft: bottomStart,
bottomRight: bottomEnd,
);
}
}
}
\ No newline at end of file
... ...
... ... @@ -20,12 +20,7 @@ import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import '../../pdf.dart';
import 'basic.dart';
import 'border_radius.dart';
import 'box_border.dart';
import 'geometry.dart';
import 'image_provider.dart';
import 'widget.dart';
import '../../widgets.dart';
enum DecorationPosition { background, foreground }
... ... @@ -271,7 +266,7 @@ class BoxDecoration {
/// The color to fill in the background of the box.
final PdfColor? color;
final BoxBorder? border;
final BorderRadius? borderRadius;
final BorderRadiusGeometry? borderRadius;
final BoxShape shape;
final DecorationGraphic? image;
final Gradient? gradient;
... ... @@ -282,11 +277,12 @@ class BoxDecoration {
PdfRect box, [
PaintPhase phase = PaintPhase.all,
]) {
final resolvedBorderRadius = borderRadius?.resolve(Directionality.of(context));
if (phase == PaintPhase.all || phase == PaintPhase.background) {
if (color != null) {
switch (shape) {
case BoxShape.rectangle:
if (borderRadius == null) {
if (resolvedBorderRadius == null) {
if (boxShadow != null) {
for (final s in boxShadow!) {
final i = PdfRasterBase.shadowRect(box.width, box.height,
... ... @@ -313,7 +309,7 @@ class BoxDecoration {
);
}
}
borderRadius!.paint(context, box);
resolvedBorderRadius.paint(context, box);
}
break;
case BoxShape.circle:
... ... @@ -341,10 +337,10 @@ class BoxDecoration {
if (gradient != null) {
switch (shape) {
case BoxShape.rectangle:
if (borderRadius == null) {
if (resolvedBorderRadius == null) {
context.canvas.drawBox(box);
} else {
borderRadius!.paint(context, box);
resolvedBorderRadius.paint(context, box);
}
break;
case BoxShape.circle:
... ... @@ -367,8 +363,8 @@ class BoxDecoration {
break;
case BoxShape.rectangle:
if (borderRadius != null) {
borderRadius!.paint(context, box);
if (resolvedBorderRadius!= null) {
resolvedBorderRadius.paint(context, box);
context.canvas.clipPath();
}
break;
... ... @@ -384,7 +380,7 @@ class BoxDecoration {
context,
box,
shape: shape,
borderRadius: borderRadius,
borderRadius: resolvedBorderRadius,
);
}
}
... ...
... ... @@ -16,14 +16,10 @@
import 'dart:math' as math;
import 'package:pdf/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
import '../../pdf.dart';
import 'basic.dart';
import 'geometry.dart';
import 'multi_page.dart';
import 'widget.dart';
import '../../widgets.dart';
enum FlexFit {
tight,
... ...
... ... @@ -146,7 +146,7 @@ class Checkbox extends SingleChildWidget with AnnotationAppearance {
BoxDecoration? decoration,
}) : radius = decoration?.shape == BoxShape.circle
? Radius.circular(math.max(height, width) / 2)
: decoration?.borderRadius?.topLeft ?? Radius.zero,
: decoration?.borderRadius?.uniform ?? Radius.zero,
super(
child: Container(
width: width,
... ...
... ... @@ -566,7 +566,35 @@ class EdgeInsetsDirectional extends EdgeInsetsGeometry {
}
}
class Alignment {
/// Base class for [Alignment] that allows for text-direction aware
/// resolution.
///
/// A property or argument of this type accepts classes created either with [
/// Alignment] and its variants, or [AlignmentDirectional.new].
///
/// To convert an [AlignmentGeometry] object of indeterminate type into an
/// [Alignment] object, call the [resolve] method.
@immutable
abstract class AlignmentGeometry {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const AlignmentGeometry();
/// Convert this instance into an [Alignment], which uses literal
/// coordinates (the `x` coordinate being explicitly a distance from the
/// left).
///
/// See also:
///
/// * [Alignment], for which this is a no-op (returns itself).
/// * [AlignmentDirectional], which flips the horizontal direction
/// based on the `direction` argument.
Alignment resolve(TextDirection? direction);
}
class Alignment extends AlignmentGeometry {
const Alignment(this.x, this.y);
/// The distance fraction in the horizontal direction.
... ... @@ -633,9 +661,169 @@ class Alignment {
}
@override
String toString() => '($x, $y)';
String toString() => _stringify(x, y);
static String _stringify(double x, double y) {
if (x == -1.0 && y == -1.0) {
return 'Alignment.topLeft';
}
if (x == 0.0 && y == -1.0) {
return 'Alignment.topCenter';
}
if (x == 1.0 && y == -1.0) {
return 'Alignment.topRight';
}
if (x == -1.0 && y == 0.0) {
return 'Alignment.centerLeft';
}
if (x == 0.0 && y == 0.0) {
return 'Alignment.center';
}
if (x == 1.0 && y == 0.0) {
return 'Alignment.centerRight';
}
if (x == -1.0 && y == 1.0) {
return 'Alignment.bottomLeft';
}
if (x == 0.0 && y == 1.0) {
return 'Alignment.bottomCenter';
}
if (x == 1.0 && y == 1.0) {
return 'Alignment.bottomRight';
}
return 'Alignment(${x.toStringAsFixed(1)}, '
'${y.toStringAsFixed(1)})';
}
@override
Alignment resolve(TextDirection? direction) => this;
}
/// An offset that's expressed as a fraction of a [Size], but whose horizontal
/// component is dependent on the writing direction.
///
/// This can be used to indicate an offset from the left in [TextDirection.ltr]
/// text and an offset from the right in [TextDirection.rtl] text without having
/// to be aware of the current text direction.
///
/// See also:
///
/// * [Alignment], a variant that is defined in physical terms (i.e.
/// whose horizontal component does not depend on the text direction).
class AlignmentDirectional extends AlignmentGeometry {
/// Creates a directional alignment.
///
/// The [start] and [y] arguments must not be null.
const AlignmentDirectional(this.start, this.y);
/// The distance fraction in the horizontal direction.
///
/// A value of -1.0 corresponds to the edge on the "start" side, which is the
/// left side in [TextDirection.ltr] contexts and the right side in
/// [TextDirection.rtl] contexts. A value of 1.0 corresponds to the opposite
/// edge, the "end" side. Values are not limited to that range; values less
/// than -1.0 represent positions beyond the start edge, and values greater than
/// 1.0 represent positions beyond the end edge.
///
/// This value is normalized into an [Alignment.x] value by the [resolve]
/// method.
final double start;
/// The distance fraction in the vertical direction.
///
/// A value of -1.0 corresponds to the topmost edge. A value of 1.0
/// corresponds to the bottommost edge. Values are not limited to that range;
/// values less than -1.0 represent positions above the top, and values
/// greater than 1.0 represent positions below the bottom.
///
/// This value is passed through to [Alignment.y] unmodified by the
/// [resolve] method.
final double y;
/// The top corner on the "start" side.
static const AlignmentDirectional topStart = AlignmentDirectional(-1.0, -1.0);
/// The center point along the top edge.
///
/// Consider using [Alignment.topCenter] instead, as it does not need
/// to be [resolve]d to be used.
static const AlignmentDirectional topCenter = AlignmentDirectional(0.0, -1.0);
/// The top corner on the "end" side.
static const AlignmentDirectional topEnd = AlignmentDirectional(1.0, -1.0);
/// The center point along the "start" edge.
static const AlignmentDirectional centerStart = AlignmentDirectional(-1.0, 0.0);
/// The center point, both horizontally and vertically.
///
/// Consider using [Alignment.center] instead, as it does not need to
/// be [resolve]d to be used.
static const AlignmentDirectional center = AlignmentDirectional(0.0, 0.0);
/// The center point along the "end" edge.
static const AlignmentDirectional centerEnd = AlignmentDirectional(1.0, 0.0);
/// The bottom corner on the "start" side.
static const AlignmentDirectional bottomStart = AlignmentDirectional(-1.0, 1.0);
/// The center point along the bottom edge.
///
/// Consider using [Alignment.bottomCenter] instead, as it does not
/// need to be [resolve]d to be used.
static const AlignmentDirectional bottomCenter = AlignmentDirectional(0.0, 1.0);
/// The bottom corner on the "end" side.
static const AlignmentDirectional bottomEnd = AlignmentDirectional(1.0, 1.0);
static String _stringify(double start, double y) {
if (start == -1.0 && y == -1.0) {
return 'AlignmentDirectional.topStart';
}
if (start == 0.0 && y == -1.0) {
return 'AlignmentDirectional.topCenter';
}
if (start == 1.0 && y == -1.0) {
return 'AlignmentDirectional.topEnd';
}
if (start == -1.0 && y == 0.0) {
return 'AlignmentDirectional.centerStart';
}
if (start == 0.0 && y == 0.0) {
return 'AlignmentDirectional.center';
}
if (start == 1.0 && y == 0.0) {
return 'AlignmentDirectional.centerEnd';
}
if (start == -1.0 && y == 1.0) {
return 'AlignmentDirectional.bottomStart';
}
if (start == 0.0 && y == 1.0) {
return 'AlignmentDirectional.bottomCenter';
}
if (start == 1.0 && y == 1.0) {
return 'AlignmentDirectional.bottomEnd';
}
return 'AlignmentDirectional(${start.toStringAsFixed(1)}, '
'${y.toStringAsFixed(1)})';
}
@override
String toString() => _stringify(start, y);
@override
Alignment resolve(TextDirection? direction) {
assert(direction != null, 'Cannot resolve $runtimeType without a TextDirection.');
switch (direction!) {
case TextDirection.rtl:
return Alignment(-start, y);
case TextDirection.ltr:
return Alignment(start, y);
}
}
}
/// An offset that's expressed as a fraction of a [PdfPoint].
@immutable
class FractionalOffset extends Alignment {
... ...
... ... @@ -16,14 +16,10 @@
import 'dart:math' as math;
import 'package:pdf/widgets.dart';
import 'package:vector_math/vector_math_64.dart';
import '../../pdf.dart';
import 'flex.dart';
import 'geometry.dart';
import 'multi_page.dart';
import 'widget.dart';
import '../../widgets.dart';
/// How [Wrap] should align objects.
enum WrapAlignment {
... ...
... ... @@ -284,6 +284,120 @@ void main() {
);
});
test('Should render a blue box aligned center right', () {
pdf.addPage(
Page(
textDirection: TextDirection.rtl,
pageFormat: const PdfPageFormat(150, 150),
build: (Context context) {
return Align(
alignment: AlignmentDirectional.centerStart,
child: _blueBox,
);
},
),
);
});
test('Should render a blue box aligned center left', () {
pdf.addPage(
Page(
textDirection: TextDirection.ltr,
pageFormat: const PdfPageFormat(150, 150),
build: (Context context) {
return Align(
alignment: AlignmentDirectional.centerStart,
child: _blueBox,
);
},
),
);
});
test('Should render a box with top-right curved corner', () {
pdf.addPage(
Page(
textDirection: TextDirection.rtl,
pageFormat: const PdfPageFormat(150, 150),
build: (Context context) {
return Container(
decoration: const BoxDecoration(
color: PdfColors.blue,
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(20),
),
),
width: 150,
height: 150,
);
},
),
);
});
test('Should render a box with right curved corners', () {
pdf.addPage(
Page(
textDirection: TextDirection.rtl,
pageFormat: const PdfPageFormat(150, 150),
build: (Context context) {
return Container(
decoration: const BoxDecoration(
color: PdfColors.blue,
borderRadius: BorderRadiusDirectional.horizontal(
start: Radius.circular(20),
),
),
width: 150,
height: 150,
);
},
),
);
});
test('Should render a box with left curved corners', () {
pdf.addPage(
Page(
textDirection: TextDirection.ltr,
pageFormat: const PdfPageFormat(150, 150),
build: (Context context) {
return Container(
decoration: const BoxDecoration(
color: PdfColors.blue,
borderRadius: BorderRadiusDirectional.horizontal(
start: Radius.circular(20),
),
),
width: 150,
height: 150,
);
},
),
);
});
test('Should render a box with top-left curved corner', () {
pdf.addPage(
Page(
textDirection: TextDirection.ltr,
pageFormat: const PdfPageFormat(150, 150),
build: (Context context) {
return Container(
decoration: const BoxDecoration(
color: PdfColors.blue,
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(20),
),
),
width: 150,
height: 150,
);
},
),
);
});
tearDownAll(() async {
final file = File('rtl-layout.pdf');
await file.writeAsBytes(await pdf.save());
... ...