Milad akarie
Committed by David PHAM-VAN

Add RTL support to BorderRadius widget

Add RTL support Alignment
@@ -16,15 +16,10 @@ @@ -16,15 +16,10 @@
16 16
17 import 'dart:math' as math; 17 import 'dart:math' as math;
18 18
19 -import 'package:pdf/widgets.dart';  
20 import 'package:vector_math/vector_math_64.dart'; 19 import 'package:vector_math/vector_math_64.dart';
21 20
22 import '../../pdf.dart'; 21 import '../../pdf.dart';
23 -import 'box_border.dart';  
24 -import 'container.dart';  
25 -import 'decoration.dart';  
26 -import 'geometry.dart';  
27 -import 'widget.dart'; 22 +import '../../widgets.dart';
28 23
29 enum BoxFit { fill, contain, cover, fitWidth, fitHeight, none, scaleDown } 24 enum BoxFit { fill, contain, cover, fitWidth, fitHeight, none, scaleDown }
30 25
@@ -303,7 +298,7 @@ class Align extends SingleChildWidget { @@ -303,7 +298,7 @@ class Align extends SingleChildWidget {
303 super(child: child); 298 super(child: child);
304 299
305 /// How to align the child. 300 /// How to align the child.
306 - final Alignment alignment; 301 + final AlignmentGeometry alignment;
307 302
308 /// If non-null, sets its width to the child's width multiplied by this factor. 303 /// If non-null, sets its width to the child's width multiplied by this factor.
309 final double? widthFactor; 304 final double? widthFactor;
@@ -330,8 +325,8 @@ class Align extends SingleChildWidget { @@ -330,8 +325,8 @@ class Align extends SingleChildWidget {
330 height: shrinkWrapHeight 325 height: shrinkWrapHeight
331 ? child!.box!.height * (heightFactor ?? 1.0) 326 ? child!.box!.height * (heightFactor ?? 1.0)
332 : double.infinity); 327 : double.infinity);
333 -  
334 - child!.box = alignment.inscribe(child!.box!.size, box!); 328 + final resolvedAlignment = alignment.resolve(Directionality.of(context));
  329 + child!.box = resolvedAlignment.inscribe(child!.box!.size, box!);
335 } else { 330 } else {
336 box = constraints.constrainRect( 331 box = constraints.constrainRect(
337 width: shrinkWrapWidth ? 0.0 : double.infinity, 332 width: shrinkWrapWidth ? 0.0 : double.infinity,
@@ -14,8 +14,10 @@ @@ -14,8 +14,10 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
  17 +import 'package:meta/meta.dart';
  18 +
17 import '../../pdf.dart'; 19 import '../../pdf.dart';
18 -import 'widget.dart'; 20 +import '../../widgets.dart';
19 21
20 /// A radius for either circular or elliptical shapes. 22 /// A radius for either circular or elliptical shapes.
21 class Radius { 23 class Radius {
@@ -35,8 +37,149 @@ class Radius { @@ -35,8 +37,149 @@ class Radius {
35 static const Radius zero = Radius.circular(0.0); 37 static const Radius zero = Radius.circular(0.0);
36 } 38 }
37 39
  40 +/// Base class for [BorderRadius] that allows for text-direction aware resolution.
  41 +///
  42 +/// A property or argument of this type accepts classes created either with [
  43 +/// BorderRadius.only] and its variants, or [BorderRadiusDirectional.only]
  44 +/// and its variants.
  45 +///
  46 +/// To convert a [BorderRadiusGeometry] object of indeterminate type into a
  47 +/// [BorderRadius] object, call the [resolve] method.
  48 +@immutable
  49 +abstract class BorderRadiusGeometry {
  50 + /// Abstract const constructor. This constructor enables subclasses to provide
  51 + /// const constructors so that they can be used in const expressions.
  52 + const BorderRadiusGeometry();
  53 +
  54 + Radius get _topLeft;
  55 +
  56 + Radius get _topRight;
  57 +
  58 + Radius get _bottomLeft;
  59 +
  60 + Radius get _bottomRight;
  61 +
  62 + Radius get _topStart;
  63 +
  64 + Radius get _topEnd;
  65 +
  66 + Radius get _bottomStart;
  67 +
  68 + Radius get _bottomEnd;
  69 +
  70 + Radius get uniform;
  71 +
  72 + bool get isUniform;
  73 +
  74 + /// Convert this instance into a [BorderRadius], so that the radii are
  75 + /// expressed for specific physical corners (top-left, top-right, etc) rather
  76 + /// than in a direction-dependent manner.
  77 + ///
  78 + /// See also:
  79 + ///
  80 + /// * [BorderRadius], for which this is a no-op (returns itself).
  81 + /// * [BorderRadiusDirectional], which flips the horizontal direction
  82 + /// based on the `direction` argument.
  83 + BorderRadius resolve(TextDirection? direction);
  84 +
  85 + @override
  86 + String toString() {
  87 + String? visual, logical;
  88 + if (_topLeft == _topRight && _topRight == _bottomLeft && _bottomLeft == _bottomRight) {
  89 + if (_topLeft != Radius.zero) {
  90 + if (_topLeft.x == _topLeft.y) {
  91 + visual = 'BorderRadius.circular(${_topLeft.x.toStringAsFixed(1)})';
  92 + } else {
  93 + visual = 'BorderRadius.all($_topLeft)';
  94 + }
  95 + }
  96 + } else {
  97 + // visuals aren't the same and at least one isn't zero
  98 + final result = StringBuffer();
  99 + result.write('BorderRadius.only(');
  100 + var comma = false;
  101 + if (_topLeft != Radius.zero) {
  102 + result.write('topLeft: $_topLeft');
  103 + comma = true;
  104 + }
  105 + if (_topRight != Radius.zero) {
  106 + if (comma) {
  107 + result.write(', ');
  108 + }
  109 + result.write('topRight: $_topRight');
  110 + comma = true;
  111 + }
  112 + if (_bottomLeft != Radius.zero) {
  113 + if (comma) {
  114 + result.write(', ');
  115 + }
  116 + result.write('bottomLeft: $_bottomLeft');
  117 + comma = true;
  118 + }
  119 + if (_bottomRight != Radius.zero) {
  120 + if (comma) {
  121 + result.write(', ');
  122 + }
  123 + result.write('bottomRight: $_bottomRight');
  124 + }
  125 + result.write(')');
  126 + visual = result.toString();
  127 + }
  128 + if (_topStart == _topEnd && _topEnd == _bottomEnd && _bottomEnd == _bottomStart) {
  129 + if (_topStart != Radius.zero) {
  130 + if (_topStart.x == _topStart.y) {
  131 + logical = 'BorderRadiusDirectional.circular(${_topStart.x.toStringAsFixed(1)})';
  132 + } else {
  133 + logical = 'BorderRadiusDirectional.all($_topStart)';
  134 + }
  135 + }
  136 + } else {
  137 + // logical aren't the same and at least one isn't zero
  138 + final result = StringBuffer();
  139 + result.write('BorderRadiusDirectional.only(');
  140 + var comma = false;
  141 + if (_topStart != Radius.zero) {
  142 + result.write('topStart: $_topStart');
  143 + comma = true;
  144 + }
  145 + if (_topEnd != Radius.zero) {
  146 + if (comma) {
  147 + result.write(', ');
  148 + }
  149 + result.write('topEnd: $_topEnd');
  150 + comma = true;
  151 + }
  152 + if (_bottomStart != Radius.zero) {
  153 + if (comma) {
  154 + result.write(', ');
  155 + }
  156 + result.write('bottomStart: $_bottomStart');
  157 + comma = true;
  158 + }
  159 + if (_bottomEnd != Radius.zero) {
  160 + if (comma) {
  161 + result.write(', ');
  162 + }
  163 + result.write('bottomEnd: $_bottomEnd');
  164 + }
  165 + result.write(')');
  166 + logical = result.toString();
  167 + }
  168 + if (visual != null && logical != null) {
  169 + return '$visual + $logical';
  170 + }
  171 + if (visual != null) {
  172 + return visual;
  173 + }
  174 + if (logical != null) {
  175 + return logical;
  176 + }
  177 + return 'BorderRadius.zero';
  178 + }
  179 +}
  180 +
38 /// An immutable set of radii for each corner of a rectangle. 181 /// An immutable set of radii for each corner of a rectangle.
39 -class BorderRadius { 182 +class BorderRadius extends BorderRadiusGeometry {
40 /// Creates a border radius where all radii are [radius]. 183 /// Creates a border radius where all radii are [radius].
41 const BorderRadius.all(Radius radius) 184 const BorderRadius.all(Radius radius)
42 : this.only( 185 : this.only(
@@ -85,6 +228,8 @@ class BorderRadius { @@ -85,6 +228,8 @@ class BorderRadius {
85 this.bottomRight = Radius.zero, 228 this.bottomRight = Radius.zero,
86 }); 229 });
87 230
  231 +
  232 +
88 /// A border radius with all zero radii. 233 /// A border radius with all zero radii.
89 static const BorderRadius zero = BorderRadius.all(Radius.zero); 234 static const BorderRadius zero = BorderRadius.all(Radius.zero);
90 235
@@ -100,6 +245,14 @@ class BorderRadius { @@ -100,6 +245,14 @@ class BorderRadius {
100 /// The bottom-right [Radius]. 245 /// The bottom-right [Radius].
101 final Radius bottomRight; 246 final Radius bottomRight;
102 247
  248 +
  249 + @override
  250 + bool get isUniform => topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight;
  251 +
  252 + @override
  253 + Radius get uniform => isUniform ? topLeft : Radius.zero;
  254 +
  255 +
103 void paint(Context context, PdfRect box) { 256 void paint(Context context, PdfRect box) {
104 // Ellipse 4-spline magic number 257 // Ellipse 4-spline magic number
105 const _m4 = 0.551784; 258 const _m4 = 0.551784;
@@ -108,23 +261,13 @@ class BorderRadius { @@ -108,23 +261,13 @@ class BorderRadius {
108 // Start 261 // Start
109 ..moveTo(box.x, box.y + bottomLeft.y) 262 ..moveTo(box.x, box.y + bottomLeft.y)
110 // bottomLeft 263 // bottomLeft
111 - ..curveTo(  
112 - box.x,  
113 - box.y - _m4 * bottomLeft.y + bottomLeft.y,  
114 - box.x - _m4 * bottomLeft.x + bottomLeft.x,  
115 - box.y,  
116 - box.x + bottomLeft.x,  
117 - box.y) 264 + ..curveTo(box.x, box.y - _m4 * bottomLeft.y + bottomLeft.y, box.x - _m4 * bottomLeft.x + bottomLeft.x, box.y,
  265 + box.x + bottomLeft.x, box.y)
118 // bottom 266 // bottom
119 ..lineTo(box.x + box.width - bottomRight.x, box.y) 267 ..lineTo(box.x + box.width - bottomRight.x, box.y)
120 // bottomRight 268 // bottomRight
121 - ..curveTo(  
122 - box.x + _m4 * bottomRight.x + box.width - bottomRight.x,  
123 - box.y,  
124 - box.x + box.width,  
125 - box.y - _m4 * bottomRight.y + bottomRight.y,  
126 - box.x + box.width,  
127 - box.y + bottomRight.y) 269 + ..curveTo(box.x + _m4 * bottomRight.x + box.width - bottomRight.x, box.y, box.x + box.width,
  270 + box.y - _m4 * bottomRight.y + bottomRight.y, box.x + box.width, box.y + bottomRight.y)
128 // right 271 // right
129 ..lineTo(box.x + box.width, box.y + box.height - topRight.y) 272 ..lineTo(box.x + box.width, box.y + box.height - topRight.y)
130 // topRight 273 // topRight
@@ -138,14 +281,165 @@ class BorderRadius { @@ -138,14 +281,165 @@ class BorderRadius {
138 // top 281 // top
139 ..lineTo(box.x + topLeft.x, box.y + box.height) 282 ..lineTo(box.x + topLeft.x, box.y + box.height)
140 // topLeft 283 // topLeft
141 - ..curveTo(  
142 - box.x - _m4 * topLeft.x + topLeft.x,  
143 - box.y + box.height,  
144 - box.x,  
145 - box.y + _m4 * topLeft.y + box.height - topLeft.y,  
146 - box.x,  
147 - box.y + box.height - topLeft.y) 284 + ..curveTo(box.x - _m4 * topLeft.x + topLeft.x, box.y + box.height, box.x,
  285 + box.y + _m4 * topLeft.y + box.height - topLeft.y, box.x, box.y + box.height - topLeft.y)
148 // left 286 // left
149 ..lineTo(box.x, box.y + bottomLeft.y); 287 ..lineTo(box.x, box.y + bottomLeft.y);
150 } 288 }
  289 +
  290 + @override
  291 + Radius get _topLeft => topLeft;
  292 +
  293 + @override
  294 + Radius get _topRight => topRight;
  295 +
  296 + @override
  297 + Radius get _bottomLeft => bottomLeft;
  298 +
  299 + @override
  300 + Radius get _bottomRight => bottomRight;
  301 +
  302 + @override
  303 + Radius get _topStart => Radius.zero;
  304 +
  305 + @override
  306 + Radius get _topEnd => Radius.zero;
  307 +
  308 + @override
  309 + Radius get _bottomStart => Radius.zero;
  310 +
  311 + @override
  312 + Radius get _bottomEnd => Radius.zero;
  313 +
  314 + @override
  315 + BorderRadius resolve(TextDirection? direction) => this;
  316 +}
  317 +
  318 +/// An immutable set of radii for each corner of a rectangle, but with the
  319 +/// corners specified in a manner dependent on the writing direction.
  320 +///
  321 +/// This can be used to specify a corner radius on the leading or trailing edge
  322 +/// of a box, so that it flips to the other side when the text alignment flips
  323 +/// (e.g. being on the top right in English text but the top left in Arabic
  324 +/// text).
  325 +///
  326 +/// See also:
  327 +///
  328 +/// * [BorderRadius], a variant that uses physical labels (`topLeft` and
  329 +/// `topRight` instead of `topStart` and `topEnd`).
  330 +class BorderRadiusDirectional extends BorderRadiusGeometry {
  331 + /// Creates a border radius where all radii are [radius].
  332 + const BorderRadiusDirectional.all(Radius radius) : this.only(
  333 + topStart: radius,
  334 + topEnd: radius,
  335 + bottomStart: radius,
  336 + bottomEnd: radius,
  337 + );
  338 +
  339 + /// Creates a border radius where all radii are [Radius.circular(radius)].
  340 + BorderRadiusDirectional.circular(double radius) : this.all(
  341 + Radius.circular(radius),
  342 + );
  343 +
  344 + /// Creates a vertically symmetric border radius where the top and bottom
  345 + /// sides of the rectangle have the same radii.
  346 + const BorderRadiusDirectional.vertical({
  347 + Radius top = Radius.zero,
  348 + Radius bottom = Radius.zero,
  349 + }) : this.only(
  350 + topStart: top,
  351 + topEnd: top,
  352 + bottomStart: bottom,
  353 + bottomEnd: bottom,
  354 + );
  355 +
  356 + /// Creates a horizontally symmetrical border radius where the start and end
  357 + /// sides of the rectangle have the same radii.
  358 + const BorderRadiusDirectional.horizontal({
  359 + Radius start = Radius.zero,
  360 + Radius end = Radius.zero,
  361 + }) : this.only(
  362 + topStart: start,
  363 + topEnd: end,
  364 + bottomStart: start,
  365 + bottomEnd: end,
  366 + );
  367 +
  368 + /// Creates a border radius with only the given non-zero values. The other
  369 + /// corners will be right angles.
  370 + const BorderRadiusDirectional.only({
  371 + this.topStart = Radius.zero,
  372 + this.topEnd = Radius.zero,
  373 + this.bottomStart = Radius.zero,
  374 + this.bottomEnd = Radius.zero,
  375 + });
  376 +
  377 + /// A border radius with all zero radii.
  378 + ///
  379 + /// Consider using [BorderRadius.zero] instead, since that object has the same
  380 + /// effect, but will be cheaper to [resolve].
  381 + static const BorderRadiusDirectional zero = BorderRadiusDirectional.all(Radius.zero);
  382 +
  383 + /// The top-start [Radius].
  384 + final Radius topStart;
  385 +
  386 + @override
  387 + Radius get _topStart => topStart;
  388 +
  389 + /// The top-end [Radius].
  390 + final Radius topEnd;
  391 +
  392 + @override
  393 + Radius get _topEnd => topEnd;
  394 +
  395 + /// The bottom-start [Radius].
  396 + final Radius bottomStart;
  397 +
  398 + @override
  399 + Radius get _bottomStart => bottomStart;
  400 +
  401 + /// The bottom-end [Radius].
  402 + final Radius bottomEnd;
  403 +
  404 + @override
  405 + Radius get _bottomEnd => bottomEnd;
  406 +
  407 + @override
  408 + Radius get _topLeft => Radius.zero;
  409 +
  410 + @override
  411 + Radius get _topRight => Radius.zero;
  412 +
  413 + @override
  414 + Radius get _bottomLeft => Radius.zero;
  415 +
  416 + @override
  417 + Radius get _bottomRight => Radius.zero;
  418 +
  419 + @override
  420 + bool get isUniform => topStart == topEnd && topStart == bottomStart && topStart == bottomEnd;
  421 +
  422 + @override
  423 + Radius get uniform => isUniform ? topStart : Radius.zero;
  424 +
  425 + @override
  426 + BorderRadius resolve(TextDirection? direction) {
  427 + assert(direction != null);
  428 + switch (direction!) {
  429 + case TextDirection.rtl:
  430 + return BorderRadius.only(
  431 + topLeft: topEnd,
  432 + topRight: topStart,
  433 + bottomLeft: bottomEnd,
  434 + bottomRight: bottomStart,
  435 + );
  436 + case TextDirection.ltr:
  437 + return BorderRadius.only(
  438 + topLeft: topStart,
  439 + topRight: topEnd,
  440 + bottomLeft: bottomStart,
  441 + bottomRight: bottomEnd,
  442 + );
  443 + }
  444 + }
151 } 445 }
@@ -20,12 +20,7 @@ import 'package:meta/meta.dart'; @@ -20,12 +20,7 @@ import 'package:meta/meta.dart';
20 import 'package:vector_math/vector_math_64.dart'; 20 import 'package:vector_math/vector_math_64.dart';
21 21
22 import '../../pdf.dart'; 22 import '../../pdf.dart';
23 -import 'basic.dart';  
24 -import 'border_radius.dart';  
25 -import 'box_border.dart';  
26 -import 'geometry.dart';  
27 -import 'image_provider.dart';  
28 -import 'widget.dart'; 23 +import '../../widgets.dart';
29 24
30 enum DecorationPosition { background, foreground } 25 enum DecorationPosition { background, foreground }
31 26
@@ -271,7 +266,7 @@ class BoxDecoration { @@ -271,7 +266,7 @@ class BoxDecoration {
271 /// The color to fill in the background of the box. 266 /// The color to fill in the background of the box.
272 final PdfColor? color; 267 final PdfColor? color;
273 final BoxBorder? border; 268 final BoxBorder? border;
274 - final BorderRadius? borderRadius; 269 + final BorderRadiusGeometry? borderRadius;
275 final BoxShape shape; 270 final BoxShape shape;
276 final DecorationGraphic? image; 271 final DecorationGraphic? image;
277 final Gradient? gradient; 272 final Gradient? gradient;
@@ -282,11 +277,12 @@ class BoxDecoration { @@ -282,11 +277,12 @@ class BoxDecoration {
282 PdfRect box, [ 277 PdfRect box, [
283 PaintPhase phase = PaintPhase.all, 278 PaintPhase phase = PaintPhase.all,
284 ]) { 279 ]) {
  280 + final resolvedBorderRadius = borderRadius?.resolve(Directionality.of(context));
285 if (phase == PaintPhase.all || phase == PaintPhase.background) { 281 if (phase == PaintPhase.all || phase == PaintPhase.background) {
286 if (color != null) { 282 if (color != null) {
287 switch (shape) { 283 switch (shape) {
288 case BoxShape.rectangle: 284 case BoxShape.rectangle:
289 - if (borderRadius == null) { 285 + if (resolvedBorderRadius == null) {
290 if (boxShadow != null) { 286 if (boxShadow != null) {
291 for (final s in boxShadow!) { 287 for (final s in boxShadow!) {
292 final i = PdfRasterBase.shadowRect(box.width, box.height, 288 final i = PdfRasterBase.shadowRect(box.width, box.height,
@@ -313,7 +309,7 @@ class BoxDecoration { @@ -313,7 +309,7 @@ class BoxDecoration {
313 ); 309 );
314 } 310 }
315 } 311 }
316 - borderRadius!.paint(context, box); 312 + resolvedBorderRadius.paint(context, box);
317 } 313 }
318 break; 314 break;
319 case BoxShape.circle: 315 case BoxShape.circle:
@@ -341,10 +337,10 @@ class BoxDecoration { @@ -341,10 +337,10 @@ class BoxDecoration {
341 if (gradient != null) { 337 if (gradient != null) {
342 switch (shape) { 338 switch (shape) {
343 case BoxShape.rectangle: 339 case BoxShape.rectangle:
344 - if (borderRadius == null) { 340 + if (resolvedBorderRadius == null) {
345 context.canvas.drawBox(box); 341 context.canvas.drawBox(box);
346 } else { 342 } else {
347 - borderRadius!.paint(context, box); 343 + resolvedBorderRadius.paint(context, box);
348 } 344 }
349 break; 345 break;
350 case BoxShape.circle: 346 case BoxShape.circle:
@@ -367,8 +363,8 @@ class BoxDecoration { @@ -367,8 +363,8 @@ class BoxDecoration {
367 363
368 break; 364 break;
369 case BoxShape.rectangle: 365 case BoxShape.rectangle:
370 - if (borderRadius != null) {  
371 - borderRadius!.paint(context, box); 366 + if (resolvedBorderRadius!= null) {
  367 + resolvedBorderRadius.paint(context, box);
372 context.canvas.clipPath(); 368 context.canvas.clipPath();
373 } 369 }
374 break; 370 break;
@@ -384,7 +380,7 @@ class BoxDecoration { @@ -384,7 +380,7 @@ class BoxDecoration {
384 context, 380 context,
385 box, 381 box,
386 shape: shape, 382 shape: shape,
387 - borderRadius: borderRadius, 383 + borderRadius: resolvedBorderRadius,
388 ); 384 );
389 } 385 }
390 } 386 }
@@ -16,14 +16,10 @@ @@ -16,14 +16,10 @@
16 16
17 import 'dart:math' as math; 17 import 'dart:math' as math;
18 18
19 -import 'package:pdf/widgets.dart';  
20 import 'package:vector_math/vector_math_64.dart'; 19 import 'package:vector_math/vector_math_64.dart';
21 20
22 import '../../pdf.dart'; 21 import '../../pdf.dart';
23 -import 'basic.dart';  
24 -import 'geometry.dart';  
25 -import 'multi_page.dart';  
26 -import 'widget.dart'; 22 +import '../../widgets.dart';
27 23
28 enum FlexFit { 24 enum FlexFit {
29 tight, 25 tight,
@@ -146,7 +146,7 @@ class Checkbox extends SingleChildWidget with AnnotationAppearance { @@ -146,7 +146,7 @@ class Checkbox extends SingleChildWidget with AnnotationAppearance {
146 BoxDecoration? decoration, 146 BoxDecoration? decoration,
147 }) : radius = decoration?.shape == BoxShape.circle 147 }) : radius = decoration?.shape == BoxShape.circle
148 ? Radius.circular(math.max(height, width) / 2) 148 ? Radius.circular(math.max(height, width) / 2)
149 - : decoration?.borderRadius?.topLeft ?? Radius.zero, 149 + : decoration?.borderRadius?.uniform ?? Radius.zero,
150 super( 150 super(
151 child: Container( 151 child: Container(
152 width: width, 152 width: width,
@@ -566,7 +566,35 @@ class EdgeInsetsDirectional extends EdgeInsetsGeometry { @@ -566,7 +566,35 @@ class EdgeInsetsDirectional extends EdgeInsetsGeometry {
566 } 566 }
567 } 567 }
568 568
569 -class Alignment { 569 +/// Base class for [Alignment] that allows for text-direction aware
  570 +/// resolution.
  571 +///
  572 +/// A property or argument of this type accepts classes created either with [
  573 +/// Alignment] and its variants, or [AlignmentDirectional.new].
  574 +///
  575 +/// To convert an [AlignmentGeometry] object of indeterminate type into an
  576 +/// [Alignment] object, call the [resolve] method.
  577 +@immutable
  578 +abstract class AlignmentGeometry {
  579 + /// Abstract const constructor. This constructor enables subclasses to provide
  580 + /// const constructors so that they can be used in const expressions.
  581 + const AlignmentGeometry();
  582 +
  583 +
  584 + /// Convert this instance into an [Alignment], which uses literal
  585 + /// coordinates (the `x` coordinate being explicitly a distance from the
  586 + /// left).
  587 + ///
  588 + /// See also:
  589 + ///
  590 + /// * [Alignment], for which this is a no-op (returns itself).
  591 + /// * [AlignmentDirectional], which flips the horizontal direction
  592 + /// based on the `direction` argument.
  593 + Alignment resolve(TextDirection? direction);
  594 +
  595 +}
  596 +
  597 +class Alignment extends AlignmentGeometry {
570 const Alignment(this.x, this.y); 598 const Alignment(this.x, this.y);
571 599
572 /// The distance fraction in the horizontal direction. 600 /// The distance fraction in the horizontal direction.
@@ -633,9 +661,169 @@ class Alignment { @@ -633,9 +661,169 @@ class Alignment {
633 } 661 }
634 662
635 @override 663 @override
636 - String toString() => '($x, $y)'; 664 + String toString() => _stringify(x, y);
  665 +
  666 + static String _stringify(double x, double y) {
  667 + if (x == -1.0 && y == -1.0) {
  668 + return 'Alignment.topLeft';
  669 + }
  670 + if (x == 0.0 && y == -1.0) {
  671 + return 'Alignment.topCenter';
  672 + }
  673 + if (x == 1.0 && y == -1.0) {
  674 + return 'Alignment.topRight';
  675 + }
  676 + if (x == -1.0 && y == 0.0) {
  677 + return 'Alignment.centerLeft';
  678 + }
  679 + if (x == 0.0 && y == 0.0) {
  680 + return 'Alignment.center';
  681 + }
  682 + if (x == 1.0 && y == 0.0) {
  683 + return 'Alignment.centerRight';
  684 + }
  685 + if (x == -1.0 && y == 1.0) {
  686 + return 'Alignment.bottomLeft';
  687 + }
  688 + if (x == 0.0 && y == 1.0) {
  689 + return 'Alignment.bottomCenter';
  690 + }
  691 + if (x == 1.0 && y == 1.0) {
  692 + return 'Alignment.bottomRight';
  693 + }
  694 + return 'Alignment(${x.toStringAsFixed(1)}, '
  695 + '${y.toStringAsFixed(1)})';
  696 + }
  697 +
  698 + @override
  699 + Alignment resolve(TextDirection? direction) => this;
  700 +}
  701 +
  702 +/// An offset that's expressed as a fraction of a [Size], but whose horizontal
  703 +/// component is dependent on the writing direction.
  704 +///
  705 +/// This can be used to indicate an offset from the left in [TextDirection.ltr]
  706 +/// text and an offset from the right in [TextDirection.rtl] text without having
  707 +/// to be aware of the current text direction.
  708 +///
  709 +/// See also:
  710 +///
  711 +/// * [Alignment], a variant that is defined in physical terms (i.e.
  712 +/// whose horizontal component does not depend on the text direction).
  713 +class AlignmentDirectional extends AlignmentGeometry {
  714 + /// Creates a directional alignment.
  715 + ///
  716 + /// The [start] and [y] arguments must not be null.
  717 + const AlignmentDirectional(this.start, this.y);
  718 +
  719 + /// The distance fraction in the horizontal direction.
  720 + ///
  721 + /// A value of -1.0 corresponds to the edge on the "start" side, which is the
  722 + /// left side in [TextDirection.ltr] contexts and the right side in
  723 + /// [TextDirection.rtl] contexts. A value of 1.0 corresponds to the opposite
  724 + /// edge, the "end" side. Values are not limited to that range; values less
  725 + /// than -1.0 represent positions beyond the start edge, and values greater than
  726 + /// 1.0 represent positions beyond the end edge.
  727 + ///
  728 + /// This value is normalized into an [Alignment.x] value by the [resolve]
  729 + /// method.
  730 + final double start;
  731 +
  732 + /// The distance fraction in the vertical direction.
  733 + ///
  734 + /// A value of -1.0 corresponds to the topmost edge. A value of 1.0
  735 + /// corresponds to the bottommost edge. Values are not limited to that range;
  736 + /// values less than -1.0 represent positions above the top, and values
  737 + /// greater than 1.0 represent positions below the bottom.
  738 + ///
  739 + /// This value is passed through to [Alignment.y] unmodified by the
  740 + /// [resolve] method.
  741 + final double y;
  742 +
  743 + /// The top corner on the "start" side.
  744 + static const AlignmentDirectional topStart = AlignmentDirectional(-1.0, -1.0);
  745 +
  746 + /// The center point along the top edge.
  747 + ///
  748 + /// Consider using [Alignment.topCenter] instead, as it does not need
  749 + /// to be [resolve]d to be used.
  750 + static const AlignmentDirectional topCenter = AlignmentDirectional(0.0, -1.0);
  751 +
  752 + /// The top corner on the "end" side.
  753 + static const AlignmentDirectional topEnd = AlignmentDirectional(1.0, -1.0);
  754 +
  755 + /// The center point along the "start" edge.
  756 + static const AlignmentDirectional centerStart = AlignmentDirectional(-1.0, 0.0);
  757 +
  758 + /// The center point, both horizontally and vertically.
  759 + ///
  760 + /// Consider using [Alignment.center] instead, as it does not need to
  761 + /// be [resolve]d to be used.
  762 + static const AlignmentDirectional center = AlignmentDirectional(0.0, 0.0);
  763 +
  764 + /// The center point along the "end" edge.
  765 + static const AlignmentDirectional centerEnd = AlignmentDirectional(1.0, 0.0);
  766 +
  767 + /// The bottom corner on the "start" side.
  768 + static const AlignmentDirectional bottomStart = AlignmentDirectional(-1.0, 1.0);
  769 +
  770 + /// The center point along the bottom edge.
  771 + ///
  772 + /// Consider using [Alignment.bottomCenter] instead, as it does not
  773 + /// need to be [resolve]d to be used.
  774 + static const AlignmentDirectional bottomCenter = AlignmentDirectional(0.0, 1.0);
  775 +
  776 + /// The bottom corner on the "end" side.
  777 + static const AlignmentDirectional bottomEnd = AlignmentDirectional(1.0, 1.0);
  778 +
  779 + static String _stringify(double start, double y) {
  780 + if (start == -1.0 && y == -1.0) {
  781 + return 'AlignmentDirectional.topStart';
  782 + }
  783 + if (start == 0.0 && y == -1.0) {
  784 + return 'AlignmentDirectional.topCenter';
  785 + }
  786 + if (start == 1.0 && y == -1.0) {
  787 + return 'AlignmentDirectional.topEnd';
  788 + }
  789 + if (start == -1.0 && y == 0.0) {
  790 + return 'AlignmentDirectional.centerStart';
  791 + }
  792 + if (start == 0.0 && y == 0.0) {
  793 + return 'AlignmentDirectional.center';
  794 + }
  795 + if (start == 1.0 && y == 0.0) {
  796 + return 'AlignmentDirectional.centerEnd';
  797 + }
  798 + if (start == -1.0 && y == 1.0) {
  799 + return 'AlignmentDirectional.bottomStart';
  800 + }
  801 + if (start == 0.0 && y == 1.0) {
  802 + return 'AlignmentDirectional.bottomCenter';
  803 + }
  804 + if (start == 1.0 && y == 1.0) {
  805 + return 'AlignmentDirectional.bottomEnd';
  806 + }
  807 + return 'AlignmentDirectional(${start.toStringAsFixed(1)}, '
  808 + '${y.toStringAsFixed(1)})';
  809 + }
  810 +
  811 + @override
  812 + String toString() => _stringify(start, y);
  813 +
  814 + @override
  815 + Alignment resolve(TextDirection? direction) {
  816 + assert(direction != null, 'Cannot resolve $runtimeType without a TextDirection.');
  817 + switch (direction!) {
  818 + case TextDirection.rtl:
  819 + return Alignment(-start, y);
  820 + case TextDirection.ltr:
  821 + return Alignment(start, y);
  822 + }
  823 + }
637 } 824 }
638 825
  826 +
639 /// An offset that's expressed as a fraction of a [PdfPoint]. 827 /// An offset that's expressed as a fraction of a [PdfPoint].
640 @immutable 828 @immutable
641 class FractionalOffset extends Alignment { 829 class FractionalOffset extends Alignment {
@@ -16,14 +16,10 @@ @@ -16,14 +16,10 @@
16 16
17 import 'dart:math' as math; 17 import 'dart:math' as math;
18 18
19 -import 'package:pdf/widgets.dart';  
20 import 'package:vector_math/vector_math_64.dart'; 19 import 'package:vector_math/vector_math_64.dart';
21 20
22 import '../../pdf.dart'; 21 import '../../pdf.dart';
23 -import 'flex.dart';  
24 -import 'geometry.dart';  
25 -import 'multi_page.dart';  
26 -import 'widget.dart'; 22 +import '../../widgets.dart';
27 23
28 /// How [Wrap] should align objects. 24 /// How [Wrap] should align objects.
29 enum WrapAlignment { 25 enum WrapAlignment {
@@ -284,6 +284,120 @@ void main() { @@ -284,6 +284,120 @@ void main() {
284 ); 284 );
285 }); 285 });
286 286
  287 + test('Should render a blue box aligned center right', () {
  288 + pdf.addPage(
  289 + Page(
  290 + textDirection: TextDirection.rtl,
  291 + pageFormat: const PdfPageFormat(150, 150),
  292 + build: (Context context) {
  293 + return Align(
  294 + alignment: AlignmentDirectional.centerStart,
  295 + child: _blueBox,
  296 + );
  297 + },
  298 + ),
  299 + );
  300 + });
  301 +
  302 + test('Should render a blue box aligned center left', () {
  303 + pdf.addPage(
  304 + Page(
  305 + textDirection: TextDirection.ltr,
  306 + pageFormat: const PdfPageFormat(150, 150),
  307 + build: (Context context) {
  308 + return Align(
  309 + alignment: AlignmentDirectional.centerStart,
  310 + child: _blueBox,
  311 + );
  312 + },
  313 + ),
  314 + );
  315 + });
  316 +
  317 + test('Should render a box with top-right curved corner', () {
  318 + pdf.addPage(
  319 + Page(
  320 + textDirection: TextDirection.rtl,
  321 + pageFormat: const PdfPageFormat(150, 150),
  322 + build: (Context context) {
  323 + return Container(
  324 + decoration: const BoxDecoration(
  325 + color: PdfColors.blue,
  326 + borderRadius: BorderRadiusDirectional.only(
  327 + topStart: Radius.circular(20),
  328 + ),
  329 + ),
  330 + width: 150,
  331 + height: 150,
  332 + );
  333 + },
  334 + ),
  335 + );
  336 + });
  337 +
  338 + test('Should render a box with right curved corners', () {
  339 + pdf.addPage(
  340 + Page(
  341 + textDirection: TextDirection.rtl,
  342 + pageFormat: const PdfPageFormat(150, 150),
  343 + build: (Context context) {
  344 + return Container(
  345 + decoration: const BoxDecoration(
  346 + color: PdfColors.blue,
  347 + borderRadius: BorderRadiusDirectional.horizontal(
  348 + start: Radius.circular(20),
  349 + ),
  350 + ),
  351 + width: 150,
  352 + height: 150,
  353 + );
  354 + },
  355 + ),
  356 + );
  357 + });
  358 +
  359 + test('Should render a box with left curved corners', () {
  360 + pdf.addPage(
  361 + Page(
  362 + textDirection: TextDirection.ltr,
  363 + pageFormat: const PdfPageFormat(150, 150),
  364 + build: (Context context) {
  365 + return Container(
  366 + decoration: const BoxDecoration(
  367 + color: PdfColors.blue,
  368 + borderRadius: BorderRadiusDirectional.horizontal(
  369 + start: Radius.circular(20),
  370 + ),
  371 + ),
  372 + width: 150,
  373 + height: 150,
  374 + );
  375 + },
  376 + ),
  377 + );
  378 + });
  379 +
  380 + test('Should render a box with top-left curved corner', () {
  381 + pdf.addPage(
  382 + Page(
  383 + textDirection: TextDirection.ltr,
  384 + pageFormat: const PdfPageFormat(150, 150),
  385 + build: (Context context) {
  386 + return Container(
  387 + decoration: const BoxDecoration(
  388 + color: PdfColors.blue,
  389 + borderRadius: BorderRadiusDirectional.only(
  390 + topStart: Radius.circular(20),
  391 + ),
  392 + ),
  393 + width: 150,
  394 + height: 150,
  395 + );
  396 + },
  397 + ),
  398 + );
  399 + });
  400 +
287 tearDownAll(() async { 401 tearDownAll(() async {
288 final file = File('rtl-layout.pdf'); 402 final file = File('rtl-layout.pdf');
289 await file.writeAsBytes(await pdf.save()); 403 await file.writeAsBytes(await pdf.save());