1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/painting.dart' hide Border;
7
8/// Border specification for [Table] widgets.
9///
10/// This is like [Border], with the addition of two sides: the inner horizontal
11/// borders between rows and the inner vertical borders between columns.
12///
13/// The sides are represented by [BorderSide] objects.
14@immutable
15class TableBorder {
16 /// Creates a border for a table.
17 ///
18 /// All the sides of the border default to [BorderSide.none].
19 const TableBorder({
20 this.top = BorderSide.none,
21 this.right = BorderSide.none,
22 this.bottom = BorderSide.none,
23 this.left = BorderSide.none,
24 this.horizontalInside = BorderSide.none,
25 this.verticalInside = BorderSide.none,
26 this.borderRadius = BorderRadius.zero,
27 });
28
29 /// A uniform border with all sides the same color and width.
30 ///
31 /// The sides default to black solid borders, one logical pixel wide.
32 factory TableBorder.all({
33 Color color = const Color(0xFF000000),
34 double width = 1.0,
35 BorderStyle style = BorderStyle.solid,
36 BorderRadius borderRadius = BorderRadius.zero,
37 }) {
38 final BorderSide side = BorderSide(color: color, width: width, style: style);
39 return TableBorder(top: side, right: side, bottom: side, left: side, horizontalInside: side, verticalInside: side, borderRadius: borderRadius);
40 }
41
42 /// Creates a border for a table where all the interior sides use the same
43 /// styling and all the exterior sides use the same styling.
44 factory TableBorder.symmetric({
45 BorderSide inside = BorderSide.none,
46 BorderSide outside = BorderSide.none,
47 }) {
48 return TableBorder(
49 top: outside,
50 right: outside,
51 bottom: outside,
52 left: outside,
53 horizontalInside: inside,
54 verticalInside: inside,
55 );
56 }
57
58 /// The top side of this border.
59 final BorderSide top;
60
61 /// The right side of this border.
62 final BorderSide right;
63
64 /// The bottom side of this border.
65 final BorderSide bottom;
66
67 /// The left side of this border.
68 final BorderSide left;
69
70 /// The horizontal interior sides of this border.
71 final BorderSide horizontalInside;
72
73 /// The vertical interior sides of this border.
74 final BorderSide verticalInside;
75
76 /// The [BorderRadius] to use when painting the corners of this border.
77 ///
78 /// It is also applied to [DataTable]'s [Material].
79 final BorderRadius borderRadius;
80
81 /// The widths of the sides of this border represented as an [EdgeInsets].
82 ///
83 /// This can be used, for example, with a [Padding] widget to inset a box by
84 /// the size of these borders.
85 EdgeInsets get dimensions {
86 return EdgeInsets.fromLTRB(left.width, top.width, right.width, bottom.width);
87 }
88
89 /// Whether all the sides of the border (outside and inside) are identical.
90 /// Uniform borders are typically more efficient to paint.
91 bool get isUniform {
92
93 final Color topColor = top.color;
94 if (right.color != topColor ||
95 bottom.color != topColor ||
96 left.color != topColor ||
97 horizontalInside.color != topColor ||
98 verticalInside.color != topColor) {
99 return false;
100 }
101
102 final double topWidth = top.width;
103 if (right.width != topWidth ||
104 bottom.width != topWidth ||
105 left.width != topWidth ||
106 horizontalInside.width != topWidth ||
107 verticalInside.width != topWidth) {
108 return false;
109 }
110
111 final BorderStyle topStyle = top.style;
112 if (right.style != topStyle ||
113 bottom.style != topStyle ||
114 left.style != topStyle ||
115 horizontalInside.style != topStyle ||
116 verticalInside.style != topStyle) {
117 return false;
118 }
119
120 return true;
121 }
122
123 /// Creates a copy of this border but with the widths scaled by the factor `t`.
124 ///
125 /// The `t` argument represents the multiplicand, or the position on the
126 /// timeline for an interpolation from nothing to `this`, with 0.0 meaning
127 /// that the object returned should be the nil variant of this object, 1.0
128 /// meaning that no change should be applied, returning `this` (or something
129 /// equivalent to `this`), and other values meaning that the object should be
130 /// multiplied by `t`. Negative values are treated like zero.
131 ///
132 /// Values for `t` are usually obtained from an [Animation<double>], such as
133 /// an [AnimationController].
134 ///
135 /// See also:
136 ///
137 /// * [BorderSide.scale], which is used to implement this method.
138 TableBorder scale(double t) {
139 return TableBorder(
140 top: top.scale(t),
141 right: right.scale(t),
142 bottom: bottom.scale(t),
143 left: left.scale(t),
144 horizontalInside: horizontalInside.scale(t),
145 verticalInside: verticalInside.scale(t),
146 );
147 }
148
149 /// Linearly interpolate between two table borders.
150 ///
151 /// If a border is null, it is treated as having only [BorderSide.none]
152 /// borders.
153 ///
154 /// {@macro dart.ui.shadow.lerp}
155 static TableBorder? lerp(TableBorder? a, TableBorder? b, double t) {
156 if (identical(a, b)) {
157 return a;
158 }
159 if (a == null) {
160 return b!.scale(t);
161 }
162 if (b == null) {
163 return a.scale(1.0 - t);
164 }
165 return TableBorder(
166 top: BorderSide.lerp(a.top, b.top, t),
167 right: BorderSide.lerp(a.right, b.right, t),
168 bottom: BorderSide.lerp(a.bottom, b.bottom, t),
169 left: BorderSide.lerp(a.left, b.left, t),
170 horizontalInside: BorderSide.lerp(a.horizontalInside, b.horizontalInside, t),
171 verticalInside: BorderSide.lerp(a.verticalInside, b.verticalInside, t),
172 );
173 }
174
175 /// Paints the border around the given [Rect] on the given [Canvas], with the
176 /// given rows and columns.
177 ///
178 /// Uniform borders are more efficient to paint than more complex borders.
179 ///
180 /// The `rows` argument specifies the vertical positions between the rows,
181 /// relative to the given rectangle. For example, if the table contained two
182 /// rows of height 100.0 each, then `rows` would contain a single value,
183 /// 100.0, which is the vertical position between the two rows (relative to
184 /// the top edge of `rect`).
185 ///
186 /// The `columns` argument specifies the horizontal positions between the
187 /// columns, relative to the given rectangle. For example, if the table
188 /// contained two columns of height 100.0 each, then `columns` would contain a
189 /// single value, 100.0, which is the vertical position between the two
190 /// columns (relative to the left edge of `rect`).
191 ///
192 /// The [verticalInside] border is only drawn if there are at least two
193 /// columns. The [horizontalInside] border is only drawn if there are at least
194 /// two rows. The horizontal borders are drawn after the vertical borders.
195 ///
196 /// The outer borders (in the order [top], [right], [bottom], [left], with
197 /// [left] above the others) are painted after the inner borders.
198 ///
199 /// The paint order is particularly notable in the case of
200 /// partially-transparent borders.
201 void paint(
202 Canvas canvas,
203 Rect rect, {
204 required Iterable<double> rows,
205 required Iterable<double> columns,
206 }) {
207 // properties can't be null
208
209 // arguments can't be null
210 assert(rows.isEmpty || (rows.first >= 0.0 && rows.last <= rect.height));
211 assert(columns.isEmpty || (columns.first >= 0.0 && columns.last <= rect.width));
212
213 if (columns.isNotEmpty || rows.isNotEmpty) {
214 final Paint paint = Paint();
215 final Path path = Path();
216
217 if (columns.isNotEmpty) {
218 switch (verticalInside.style) {
219 case BorderStyle.solid:
220 paint
221 ..color = verticalInside.color
222 ..strokeWidth = verticalInside.width
223 ..style = PaintingStyle.stroke;
224 path.reset();
225 for (final double x in columns) {
226 path.moveTo(rect.left + x, rect.top);
227 path.lineTo(rect.left + x, rect.bottom);
228 }
229 canvas.drawPath(path, paint);
230 case BorderStyle.none:
231 break;
232 }
233 }
234
235 if (rows.isNotEmpty) {
236 switch (horizontalInside.style) {
237 case BorderStyle.solid:
238 paint
239 ..color = horizontalInside.color
240 ..strokeWidth = horizontalInside.width
241 ..style = PaintingStyle.stroke;
242 path.reset();
243 for (final double y in rows) {
244 path.moveTo(rect.left, rect.top + y);
245 path.lineTo(rect.right, rect.top + y);
246 }
247 canvas.drawPath(path, paint);
248 case BorderStyle.none:
249 break;
250 }
251 }
252 }
253 if (!isUniform || borderRadius == BorderRadius.zero) {
254 paintBorder(canvas, rect, top: top, right: right, bottom: bottom, left: left);
255 } else {
256 final RRect outer = borderRadius.toRRect(rect);
257 final RRect inner = outer.deflate(top.width);
258 final Paint paint = Paint()..color = top.color;
259 canvas.drawDRRect(outer, inner, paint);
260 }
261 }
262
263 @override
264 bool operator ==(Object other) {
265 if (identical(this, other)) {
266 return true;
267 }
268 if (other.runtimeType != runtimeType) {
269 return false;
270 }
271 return other is TableBorder
272 && other.top == top
273 && other.right == right
274 && other.bottom == bottom
275 && other.left == left
276 && other.horizontalInside == horizontalInside
277 && other.verticalInside == verticalInside
278 && other.borderRadius == borderRadius;
279 }
280
281 @override
282 int get hashCode => Object.hash(top, right, bottom, left, horizontalInside, verticalInside, borderRadius);
283
284 @override
285 String toString() => 'TableBorder($top, $right, $bottom, $left, $horizontalInside, $verticalInside, $borderRadius)';
286}
287