// 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 (identical(a, b)) { return a; } 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(); } }