Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions modules/ensemble/lib/controller/controller_mixins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ mixin HasBorderController on EnsembleWidgetController {
Color? borderColor;
int? borderWidth;
EBorderRadius? borderRadius;
BorderStyleComposite? borderStyle;

Map<String, Function> hasBorderSetters() => {
'borderGradient': (value) =>
borderGradient = Utils.getBackgroundGradient(value),
'borderColor': (value) => borderColor = Utils.getColor(value),
'borderWidth': (value) => borderWidth = Utils.optionalInt(value),
'borderRadius': (value) => borderRadius = Utils.getBorderRadius(value),
'borderStyle': (value) =>
borderStyle = BorderStyleComposite.from(this, value),
};

bool hasBorder() =>
Expand Down
71 changes: 47 additions & 24 deletions modules/ensemble/lib/widget/helpers/box_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,37 @@ import 'package:ensemble/widget/helpers/controllers.dart';
import 'package:ensemble/widget/helpers/widgets.dart';
import 'package:flutter/material.dart';

BoxBorder? _resolveBoxBorder({
required bool hasBorder,
required LinearGradient? borderGradient,
required Color? borderColor,
required int? borderWidth,
required BorderStyleComposite? borderStyle,
required BuildContext context,
}) {
if (!hasBorder) return null;

final width =
borderWidth?.toDouble() ?? ThemeManager().getBorderThickness(context);

if (borderGradient != null) {
return GradientBoxBorder(gradient: borderGradient, width: width);
}

final color = borderColor ?? ThemeManager().getBorderColor(context);
if (borderStyle != null && borderStyle.isNonSolid) {
return DashedBoxBorder(
color: color,
width: width,
style: borderStyle.type,
dashLength: borderStyle.effectiveLength,
gap: borderStyle.effectiveGap,
);
}

return Border.all(color: color, width: width);
}

/// TODO: Legacy - move to EnsembleBoxWrapper
/// wraps around a widget and gives it common box attributes
class BoxWrapper extends StatelessWidget {
Expand Down Expand Up @@ -76,18 +107,14 @@ class BoxWrapper extends StatelessWidget {
: BoxDecoration(
color: boxController.backgroundColor,
gradient: boxController.backgroundGradient,
border: !boxController.hasBorder()
? null
: boxController.borderGradient != null
? GradientBoxBorder(
gradient: boxController.borderGradient!,
width: boxController.borderWidth?.toDouble() ??
ThemeManager().getBorderThickness(context))
: Border.all(
color: boxController.borderColor ??
ThemeManager().getBorderColor(context),
width: boxController.borderWidth?.toDouble() ??
ThemeManager().getBorderThickness(context)),
border: _resolveBoxBorder(
hasBorder: boxController.hasBorder(),
borderGradient: boxController.borderGradient,
borderColor: boxController.borderColor,
borderWidth: boxController.borderWidth,
borderStyle: boxController.borderStyle,
context: context,
),
borderRadius: boxController.borderRadius?.getValue(),
boxShadow: !boxController.hasBoxShadow()
? null
Expand Down Expand Up @@ -271,18 +298,14 @@ class EnsembleBoxWrapper extends StatelessWidget {
: BoxDecoration(
color: boxController.backgroundColor,
gradient: boxController.backgroundGradient,
border: !boxController.hasBorder()
? null
: boxController.borderGradient != null
? GradientBoxBorder(
gradient: boxController.borderGradient!,
width: boxController.borderWidth?.toDouble() ??
ThemeManager().getBorderThickness(context))
: Border.all(
color: boxController.borderColor ??
ThemeManager().getBorderColor(context),
width: boxController.borderWidth?.toDouble() ??
ThemeManager().getBorderThickness(context)),
border: _resolveBoxBorder(
hasBorder: boxController.hasBorder(),
borderGradient: boxController.borderGradient,
borderColor: boxController.borderColor,
borderWidth: boxController.borderWidth,
borderStyle: boxController.borderStyle,
context: context,
),
borderRadius: _borderRadius?.getValue(),
boxShadow: boxController.boxShadow == null
? null
Expand Down
69 changes: 69 additions & 0 deletions modules/ensemble/lib/widget/helpers/controllers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,65 @@ abstract class WidgetCompositeProperty with Invokable {
}
}

/// Nested borderStyle map: type / length / gap
class BorderStyleComposite extends WidgetCompositeProperty {
BorderStyleComposite._(super.widgetController,
{BoxBorderStyle? type, this.length, this.gap})
: type = type ?? BoxBorderStyle.solid;

static const double defaultLength = 5;
static const double defaultGap = 3;

BoxBorderStyle type;
double? length;
double? gap;

factory BorderStyleComposite.from(
ChangeNotifier widgetController, dynamic payload) {
if (payload is Map) {
return BorderStyleComposite._(
widgetController,
type: BoxBorderStyle.values.from(payload['type']) ?? BoxBorderStyle.solid,
length: Utils.optionalDouble(payload['length']),
gap: Utils.optionalDouble(payload['gap']),
);
}
// shorthand: borderStyle: dashed
if (payload is String) {
return BorderStyleComposite._(
widgetController,
type: BoxBorderStyle.values.from(payload) ?? BoxBorderStyle.solid,
);
}
return BorderStyleComposite._(widgetController);
}

bool get isNonSolid =>
type == BoxBorderStyle.dashed || type == BoxBorderStyle.dotted;

double get effectiveLength => length ?? defaultLength;

double get effectiveGap => gap ?? defaultGap;

@override
Map<String, Function> getters() => {
'type': () => type.name,
'length': () => length,
'gap': () => gap,
};

@override
Map<String, Function> methods() => {};

@override
Map<String, Function> setters() => {
'type': (value) =>
type = BoxBorderStyle.values.from(value) ?? BoxBorderStyle.solid,
'length': (value) => length = Utils.optionalDouble(value),
'gap': (value) => gap = Utils.optionalDouble(value),
};
}

class BoxShadowComposite extends WidgetCompositeProperty {
BoxShadowComposite(super.widgetController, {required Map inputs}) {
color = inputs['color'];
Expand Down Expand Up @@ -409,6 +468,7 @@ class BoxController extends WidgetController {
int? borderWidth;
EBorderRadius? borderRadius;
LinearGradient? borderGradient;
BorderStyleComposite? borderStyle;

BoxShadowComposite? boxShadow;

Expand Down Expand Up @@ -449,6 +509,8 @@ class BoxController extends WidgetController {
'borderColor': (value) => borderColor = Utils.getColor(value),
'borderWidth': (value) => borderWidth = Utils.optionalInt(value),
'borderRadius': (value) => borderRadius = Utils.getBorderRadius(value),
'borderStyle': (value) =>
borderStyle = BorderStyleComposite.from(this, value),

'boxShadow': (value) =>
boxShadow = Utils.getBoxShadowComposite(this, value),
Expand Down Expand Up @@ -676,6 +738,13 @@ class EnsembleBoxController extends EnsembleWidgetController
boxShadow != null;
}

/// Border stroke pattern for container box styles.
enum BoxBorderStyle {
solid,
dashed,
dotted,
}

class EnsembleSemantics {
bool focusable;
String? label;
Expand Down
86 changes: 86 additions & 0 deletions modules/ensemble/lib/widget/helpers/widgets.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// This class contains common widgets for use with Ensemble widgets.
import 'package:ensemble/widget/helpers/controllers.dart';
import 'package:flutter/material.dart';

/// Display a Text content followed by a clear icon.
Expand Down Expand Up @@ -90,6 +91,91 @@ class GradientBoxBorder extends BoxBorder with GradientBorder {
}
}

/// Draws dashed or dotted borders for container box styles.
class DashedBoxBorder extends BoxBorder {
const DashedBoxBorder({
required this.color,
required this.width,
required this.style,
this.dashLength = BorderStyleComposite.defaultLength,
this.gap = BorderStyleComposite.defaultGap,
});

final Color color;
final double width;
final BoxBorderStyle style;
final double dashLength;
final double gap;

@override
BorderSide get bottom => BorderSide.none;

@override
BorderSide get top => BorderSide.none;

@override
bool get isUniform => true;

@override
EdgeInsetsGeometry get dimensions => EdgeInsets.all(width);

@override
void paint(
Canvas canvas,
Rect rect, {
TextDirection? textDirection,
BoxShape shape = BoxShape.rectangle,
BorderRadius? borderRadius,
}) {
final path = Path();
if (borderRadius != null) {
path.addRRect(borderRadius.toRRect(rect).deflate(width / 2));
} else {
path.addRect(rect.deflate(width / 2));
}

final paint = Paint()
..color = color
..strokeWidth = width
..style = PaintingStyle.stroke
..strokeCap =
style == BoxBorderStyle.dotted ? StrokeCap.round : StrokeCap.butt;

final dash =
style == BoxBorderStyle.dotted ? width : dashLength;
final gapLength = style == BoxBorderStyle.dotted
? (gap < width ? width : gap)
: gap;

for (final metric in path.computeMetrics()) {
double distance = 0;
bool draw = true;
while (distance < metric.length) {
final segment = draw ? dash : gapLength;
final next = distance + segment;
if (draw) {
canvas.drawPath(
metric.extractPath(distance, next.clamp(0, metric.length)),
paint);
}
distance = next;
draw = !draw;
}
}
}

@override
ShapeBorder scale(double t) {
return DashedBoxBorder(
color: color,
width: width * t,
style: style,
dashLength: dashLength * t,
gap: gap * t,
);
}
}

/// a wrapper around a widget and enable Tap action.
class TapOverlay extends StatelessWidget {
const TapOverlay({super.key, required this.widget, required this.onTap});
Expand Down
Loading