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; | ||
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()); |
-
Please register or login to post a comment