Committed by
David PHAM-VAN
Add RTL support to BorderRadius widget
Add RTL support Alignment
Showing
8 changed files
with
638 additions
and
59 deletions
| @@ -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; | ||
| 151 | } | 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 | + } | ||
| 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()); |
-
Please register or login to post a comment