1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
import 'borders.dart';
import 'edge_insets.dart';
/// Defines the relative size and alignment of one <LinearBorder> edge.
///
/// A [LinearBorder] defines a box outline as zero to four edges, each
/// of which is rendered as a single line. The width and color of the
/// lines is defined by [LinearBorder.side].
///
/// Each line's length is defined by [size], a value between 0.0 and 1.0
/// (the default) which defines the length as a percentage of the
/// length of a box edge.
///
/// When [size] is less than 1.0, the line is aligned within the
/// available space according to [alignment], a value between -1.0 and
/// 1.0. The default is 0.0, which means centered, -1.0 means align on the
/// "start" side, and 1.0 means align on the "end" side. The meaning of
/// start and end depend on the current [TextDirection], see
/// [Directionality].
@immutable
class LinearBorderEdge {
/// Defines one side of a [LinearBorder].
///
/// The values of [size] and [alignment] must be between
/// 0.0 and 1.0, and -1.0 and 1.0 respectively.
const LinearBorderEdge({
this.size = 1.0,
this.alignment = 0.0,
}) : assert(size >= 0.0 && size <= 1.0);
/// A value between 0.0 and 1.0 that defines the length of the edge as a
/// percentage of the length of the corresponding box
/// edge. Default is 1.0.
final double size;
/// A value between -1.0 and 1.0 that defines how edges for which [size]
/// is less than 1.0 are aligned relative to the corresponding box edge.
///
/// * -1.0, aligned in the "start" direction. That's left
/// for [TextDirection.ltr] and right for [TextDirection.rtl].
/// * 0.0, centered.
/// * 1.0, aligned in the "end" direction. That's right
/// for [TextDirection.ltr] and left for [TextDirection.rtl].
final double alignment;
/// Linearly interpolates between two [LinearBorder]s.
///
/// If both `a` and `b` are null then null is returned. If `a` is null
/// then we interpolate to `b` varying [size] from 0.0 to `b.size`. If `b`
/// is null then we interpolate from `a` varying size from `a.size` to zero.
/// Otherwise both values are interpolated.
static LinearBorderEdge? lerp(LinearBorderEdge? a, LinearBorderEdge? b, double t) {
if (a == null && b == null) {
return null;
}
a ??= LinearBorderEdge(alignment: b!.alignment, size: 0);
b ??= LinearBorderEdge(alignment: a.alignment, size: 0);
return LinearBorderEdge(
size: lerpDouble(a.size, b.size, t)!,
alignment: lerpDouble(a.alignment, b.alignment, t)!,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is LinearBorderEdge
&& other.size == size
&& other.alignment == alignment;
}
@override
int get hashCode => Object.hash(size, alignment);
@override
String toString() {
final StringBuffer s = StringBuffer('${objectRuntimeType(this, 'LinearBorderEdge')}(');
if (size != 1.0 ) {
s.write('size: $size');
}
if (alignment != 0) {
final String comma = size != 1.0 ? ', ' : '';
s.write('${comma}alignment: $alignment');
}
s.write(')');
return s.toString();
}
}
/// An [OutlinedBorder] like [BoxBorder] that allows one to define a rectangular (box) border
/// in terms of zero to four [LinearBorderEdge]s, each of which is rendered as a single line.
///
/// The color and width of each line are defined by [side]. When [LinearBorder] is used
/// with a class whose border sides and shape are defined by a [ButtonStyle], then a non-null
/// [ButtonStyle.side] will override the one specified here. For example the [LinearBorder]
/// in the [TextButton] example below adds a red underline to the button. This is because
/// TextButton's `side` parameter overrides the `side` property of its [ButtonStyle.shape].
///
/// ```dart
/// TextButton(
/// style: TextButton.styleFrom(
/// side: const BorderSide(color: Colors.red),
/// shape: const LinearBorder(
/// side: BorderSide(color: Colors.blue),
/// bottom: LinearBorderEdge(),
/// ),
/// ),
/// onPressed: () { },
/// child: const Text('Red LinearBorder'),
/// )
///```
///
/// This class resolves itself against the current [TextDirection] (see [Directionality]).
/// Start and end values resolve to left and right for [TextDirection.ltr] and to
/// right and left for [TextDirection.rtl].
///
/// Convenience constructors are included for the common case where just one edge is specified:
/// [LinearBorder.start], [LinearBorder.end], [LinearBorder.top], [LinearBorder.bottom].
class LinearBorder extends OutlinedBorder {
/// Creates a rectangular box border that's rendered as zero to four lines.
const LinearBorder({
super.side,
this.start,
this.end,
this.top,
this.bottom,
});
/// Creates a rectangular box border with an edge on the left for [TextDirection.ltr]
/// or on the right for [TextDirection.rtl].
LinearBorder.start({
super.side,
double alignment = 0.0,
double size = 1.0
}) : start = LinearBorderEdge(alignment: alignment, size: size),
end = null,
top = null,
bottom = null;
/// Creates a rectangular box border with an edge on the right for [TextDirection.ltr]
/// or on the left for [TextDirection.rtl].
LinearBorder.end({
super.side,
double alignment = 0.0,
double size = 1.0
}) : start = null,
end = LinearBorderEdge(alignment: alignment, size: size),
top = null,
bottom = null;
/// Creates a rectangular box border with an edge on the top.
LinearBorder.top({
super.side,
double alignment = 0.0,
double size = 1.0
}) : start = null,
end = null,
top = LinearBorderEdge(alignment: alignment, size: size),
bottom = null;
/// Creates a rectangular box border with an edge on the bottom.
LinearBorder.bottom({
super.side,
double alignment = 0.0,
double size = 1.0
}) : start = null,
end = null,
top = null,
bottom = LinearBorderEdge(alignment: alignment, size: size);
/// No border.
static const LinearBorder none = LinearBorder();
/// Defines the left edge for [TextDirection.ltr] or the right
/// for [TextDirection.rtl].
final LinearBorderEdge? start;
/// Defines the right edge for [TextDirection.ltr] or the left
/// for [TextDirection.rtl].
final LinearBorderEdge? end;
/// Defines the top edge.
final LinearBorderEdge? top;
/// Defines the bottom edge.
final LinearBorderEdge? bottom;
@override
LinearBorder scale(double t) {
return LinearBorder(
side: side.scale(t),
);
}
@override
EdgeInsetsGeometry get dimensions {
final double width = side.width;
return EdgeInsetsDirectional.fromSTEB(
start == null ? 0.0 : width,
top == null ? 0.0 : width,
end == null ? 0.0 : width,
bottom == null ? 0.0 : width,
);
}
@override
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
if (a is LinearBorder) {
return LinearBorder(
side: BorderSide.lerp(a.side, side, t),
start: LinearBorderEdge.lerp(a.start, start, t),
end: LinearBorderEdge.lerp(a.end, end, t),
top: LinearBorderEdge.lerp(a.top, top, t),
bottom: LinearBorderEdge.lerp(a.bottom, bottom, t),
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
if (b is LinearBorder) {
return LinearBorder(
side: BorderSide.lerp(side, b.side, t),
start: LinearBorderEdge.lerp(start, b.start, t),
end: LinearBorderEdge.lerp(end, b.end, t),
top: LinearBorderEdge.lerp(top, b.top, t),
bottom: LinearBorderEdge.lerp(bottom, b.bottom, t),
);
}
return super.lerpTo(b, t);
}
/// Returns a copy of this LinearBorder with the given fields replaced with
/// the new values.
@override
LinearBorder copyWith({
BorderSide? side,
LinearBorderEdge? start,
LinearBorderEdge? end,
LinearBorderEdge? top,
LinearBorderEdge? bottom,
}) {
return LinearBorder(
side: side ?? this.side,
start: start ?? this.start,
end: end ?? this.end,
top: top ?? this.top,
bottom: bottom ?? this.bottom,
);
}
@override
Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
final Rect adjustedRect = dimensions.resolve(textDirection).deflateRect(rect);
return Path()
..addRect(adjustedRect);
}
@override
Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
return Path()
..addRect(rect);
}
@override
void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
final EdgeInsets insets = dimensions.resolve(textDirection);
final bool rtl = textDirection == TextDirection.rtl;
final Path path = Path();
final Paint paint = Paint()
..strokeWidth = 0.0;
void drawEdge(Rect rect, Color color) {
paint.color = color;
path.reset();
path.moveTo(rect.left, rect.top);
if (rect.width == 0.0) {
paint.style = PaintingStyle.stroke;
path.lineTo(rect.left, rect.bottom);
} else if (rect.height == 0.0) {
paint.style = PaintingStyle.stroke;
path.lineTo(rect.right, rect.top);
} else {
paint.style = PaintingStyle.fill;
path.lineTo(rect.right, rect.top);
path.lineTo(rect.right, rect.bottom);
path.lineTo(rect.left, rect.bottom);
}
canvas.drawPath(path, paint);
}
if (start != null && start!.size != 0.0 && side.style != BorderStyle.none) {
final Rect insetRect = Rect.fromLTWH(rect.left, rect.top + insets.top, rect.width, rect.height - insets.vertical);
final double x = rtl ? rect.right - insets.right : rect.left;
final double width = rtl ? insets.right : insets.left;
final double height = insetRect.height * start!.size;
final double y = (insetRect.height - height) * ((start!.alignment + 1.0) / 2.0);
final Rect r = Rect.fromLTWH(x, y, width, height);
drawEdge(r, side.color);
}
if (end != null && end!.size != 0.0 && side.style != BorderStyle.none) {
final Rect insetRect = Rect.fromLTWH(rect.left, rect.top + insets.top, rect.width, rect.height - insets.vertical);
final double x = rtl ? rect.left : rect.right - insets.right;
final double width = rtl ? insets.left : insets.right;
final double height = insetRect.height * end!.size;
final double y = (insetRect.height - height) * ((end!.alignment + 1.0) / 2.0);
final Rect r = Rect.fromLTWH(x, y, width, height);
drawEdge(r, side.color);
}
if (top != null && top!.size != 0.0 && side.style != BorderStyle.none) {
final double width = rect.width * top!.size;
final double startX = (rect.width - width) * ((top!.alignment + 1.0) / 2.0);
final double x = rtl ? rect.width - startX - width : startX;
final Rect r = Rect.fromLTWH(x, rect.top, width, insets.top);
drawEdge(r, side.color);
}
if (bottom != null && bottom!.size != 0.0 && side.style != BorderStyle.none) {
final double width = rect.width * bottom!.size;
final double startX = (rect.width - width) * ((bottom!.alignment + 1.0) / 2.0);
final double x = rtl ? rect.width - startX - width: startX;
final Rect r = Rect.fromLTWH(x, rect.bottom - insets.bottom, width, side.width);
drawEdge(r, side.color);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is LinearBorder
&& other.side == side
&& other.start == start
&& other.end == end
&& other.top == top
&& other.bottom == bottom;
}
@override
int get hashCode => Object.hash(side, start, end, top, bottom);
@override
String toString() {
if (this == LinearBorder.none) {
return 'LinearBorder.none';
}
final StringBuffer s = StringBuffer('${objectRuntimeType(this, 'LinearBorder')}(side: $side');
if (start != null ) {
s.write(', start: $start');
}
if (end != null ) {
s.write(', end: $end');
}
if (top != null ) {
s.write(', top: $top');
}
if (bottom != null ) {
s.write(', bottom: $bottom');
}
s.write(')');
return s.toString();
}
}