From c4abae355bf192f4ea01503a398bedd09535b66e Mon Sep 17 00:00:00 2001 From: Muhammad Usman Date: Thu, 10 Sep 2026 04:29:45 +0500 Subject: [PATCH 1/3] feat(border): support decimal border widths and radii --- .../lib/controller/controller_mixins.dart | 4 +- modules/ensemble/lib/framework/model.dart | 18 +++---- .../lib/framework/theme/theme_loader.dart | 29 +++++------ modules/ensemble/lib/util/utils.dart | 52 ++++++++++++------- modules/ensemble/lib/widget/checkbox.dart | 11 ++-- .../lib/widget/helpers/box_wrapper.dart | 7 ++- .../lib/widget/helpers/controllers.dart | 4 +- .../lib/widget/helpers/form_helper.dart | 8 +-- .../ensemble/lib/widget/input/form_time.dart | 6 +-- modules/ensemble/lib/widget/shape.dart | 2 +- 10 files changed, 75 insertions(+), 66 deletions(-) diff --git a/modules/ensemble/lib/controller/controller_mixins.dart b/modules/ensemble/lib/controller/controller_mixins.dart index 7a34fdc6b..4971d039a 100644 --- a/modules/ensemble/lib/controller/controller_mixins.dart +++ b/modules/ensemble/lib/controller/controller_mixins.dart @@ -16,7 +16,7 @@ mixin HasPassThrough on EnsembleWidgetController { mixin HasBorderController on EnsembleWidgetController { LinearGradient? borderGradient; Color? borderColor; - int? borderWidth; + double? borderWidth; EBorderRadius? borderRadius; BorderStyleComposite? borderStyle; @@ -24,7 +24,7 @@ mixin HasBorderController on EnsembleWidgetController { 'borderGradient': (value) => borderGradient = Utils.getBackgroundGradient(value), 'borderColor': (value) => borderColor = Utils.getColor(value), - 'borderWidth': (value) => borderWidth = Utils.optionalInt(value), + 'borderWidth': (value) => borderWidth = Utils.optionalDouble(value), 'borderRadius': (value) => borderRadius = Utils.getBorderRadius(value), 'borderStyle': (value) => borderStyle = BorderStyleComposite.from(this, value), diff --git a/modules/ensemble/lib/framework/model.dart b/modules/ensemble/lib/framework/model.dart index 65f63aa06..042cfd2d8 100644 --- a/modules/ensemble/lib/framework/model.dart +++ b/modules/ensemble/lib/framework/model.dart @@ -97,37 +97,37 @@ class BackgroundImage { class EBorderRadius { EBorderRadius._( - int _topLeft, int _topRight, int _bottomRight, int _bottomLeft) + double _topLeft, double _topRight, double _bottomRight, double _bottomLeft) : topLeft = - _topLeft == 0 ? Radius.zero : Radius.circular(_topLeft.toDouble()), + _topLeft == 0 ? Radius.zero : Radius.circular(_topLeft), topRight = _topRight == 0 ? Radius.zero - : Radius.circular(_topRight.toDouble()), + : Radius.circular(_topRight), bottomRight = _bottomRight == 0 ? Radius.zero - : Radius.circular(_bottomRight.toDouble()), + : Radius.circular(_bottomRight), bottomLeft = _bottomLeft == 0 ? Radius.zero - : Radius.circular(_bottomLeft.toDouble()); + : Radius.circular(_bottomLeft); Radius topLeft, topRight, bottomRight, bottomLeft; - factory EBorderRadius.all(int val) { + factory EBorderRadius.all(double val) { return EBorderRadius._(val, val, val, val); } // first value: top-left & bottom-right // second value: top-right & bottom-left - factory EBorderRadius.two(int first, int second) { + factory EBorderRadius.two(double first, double second) { return EBorderRadius._(first, second, first, second); } // first value: top-left // second value: top-right & bottom-left // third value: bottom-right - factory EBorderRadius.three(int first, int second, int third) { + factory EBorderRadius.three(double first, double second, double third) { return EBorderRadius._(first, second, third, second); } factory EBorderRadius.only( - int topLeft, int topRight, int bottomRight, int bottomLeft) { + double topLeft, double topRight, double bottomRight, double bottomLeft) { return EBorderRadius._(topLeft, topRight, bottomRight, bottomLeft); } diff --git a/modules/ensemble/lib/framework/theme/theme_loader.dart b/modules/ensemble/lib/framework/theme/theme_loader.dart index 4357013e5..ca1248dc2 100644 --- a/modules/ensemble/lib/framework/theme/theme_loader.dart +++ b/modules/ensemble/lib/framework/theme/theme_loader.dart @@ -286,7 +286,7 @@ mixin ThemeLoader { BorderRadius borderRadius = Utils.getBorderRadius(input['borderRadius'])?.getValue() ?? getInputDefaultBorderRadius(variant); - int borderWidth = Utils.optionalInt(input['borderWidth']) ?? 1; + double borderWidth = Utils.optionalDouble(input['borderWidth']) ?? 1; Color? borderColor = Utils.getColor(input['borderColor']); Color? disabledBorderColor = Utils.getColor(input['disabledBorderColor']); @@ -305,7 +305,7 @@ mixin ThemeLoader { (colorScheme.brightness == Brightness.light ? Colors.black54 : Colors.white70), - width: borderWidth.toDouble())); + width: borderWidth)); return baseInputDecoration.copyWith( contentPadding: contentPadding ?? @@ -341,7 +341,7 @@ mixin ThemeLoader { (colorScheme.brightness == Brightness.light ? Colors.black87 : Colors.white70), - width: borderWidth.toDouble())); + width: borderWidth)); return baseInputDecoration.copyWith( contentPadding: contentPadding ?? const EdgeInsets.symmetric(vertical: 15, horizontal: 3), @@ -386,15 +386,13 @@ mixin ThemeLoader { isOutline ? null : Utils.getColor(input['backgroundColor']); RoundedRectangleBorder border = RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Utils.getInt(input['borderRadius'], fallback: _buttonBorderRadius) - .toDouble()), + borderRadius: Utils.getBorderRadius(input['borderRadius'])?.getValue() ?? + BorderRadius.circular(_buttonBorderRadius.toDouble()), side: borderColor == null ? BorderSide.none : BorderSide( color: borderColor, - width: Utils.getInt(input['borderWidth'], fallback: 1) - .toDouble())); + width: Utils.optionalDouble(input['borderWidth']) ?? 1)); return getButtonStyle( isOutline: isOutline, @@ -407,20 +405,20 @@ mixin ThemeLoader { InputBorder? getInputBorder( {InputVariant? variant, Color? borderColor, - required int borderWidth, + required double borderWidth, required BorderRadius borderRadius}) { if (borderColor != null) { if (variant == InputVariant.box) { return OutlineInputBorder( borderRadius: borderRadius, borderSide: - BorderSide(color: borderColor, width: borderWidth.toDouble())); + BorderSide(color: borderColor, width: borderWidth)); } // default is underline return UnderlineInputBorder( borderRadius: borderRadius, borderSide: - BorderSide(color: borderColor, width: borderWidth.toDouble())); + BorderSide(color: borderColor, width: borderWidth)); } return null; } @@ -463,21 +461,20 @@ mixin ThemeLoader { Color? fillColor = Utils.getColor(input?["fillColor"]); Color? activeColor = Utils.getColor(input?["activeColor"]); Color? checkColor = Utils.getColor(input?["checkColor"]); - int borderWidth = Utils.optionalInt(input?['borderWidth'], min: 0) ?? 2; + double borderWidth = Utils.optionalDouble(input?['borderWidth'], min: 0) ?? 2; var checkboxTheme = CheckboxThemeData( side: WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return BorderSide( - width: borderWidth.toDouble(), color: DesignSystem.disableColor); + width: borderWidth, color: DesignSystem.disableColor); } if (states.contains(WidgetState.error)) { return BorderSide( - width: borderWidth.toDouble(), - color: DesignSystem.inputErrorColor); + width: borderWidth, color: DesignSystem.inputErrorColor); } if (!states.contains(WidgetState.selected)) { - return BorderSide(width: borderWidth.toDouble(), color: borderColor); + return BorderSide(width: borderWidth, color: borderColor); } // use default return null; diff --git a/modules/ensemble/lib/util/utils.dart b/modules/ensemble/lib/util/utils.dart index 695b021d5..5926f1cc6 100644 --- a/modules/ensemble/lib/util/utils.dart +++ b/modules/ensemble/lib/util/utils.dart @@ -863,39 +863,39 @@ class Utils { } static BorderRadiusGeometry? getBorderRadiusGeometry(dynamic value) { - if (value is int) { + if (value is num) { // Optimize: Ignore zero border radius as it causes unnecessary clipping if (value != 0) { return BorderRadius.all(Radius.circular(value.toDouble())); } } else if (value is String) { - // Convert the string to a list of integers - List numbers = stringToIntegers(value, min: 0); + // Convert the string to a list of non-negative numbers + List numbers = stringToDoubles(value, min: 0); // Handle 1 to 4 values for BorderRadius switch (numbers.length) { case 1: - return BorderRadius.all(Radius.circular(numbers[0].toDouble())); + return BorderRadius.all(Radius.circular(numbers[0])); case 2: return BorderRadius.vertical( - top: Radius.circular(numbers[0].toDouble()), - bottom: Radius.circular(numbers[1].toDouble()), + top: Radius.circular(numbers[0]), + bottom: Radius.circular(numbers[1]), ); case 3: return BorderRadius.only( - topLeft: Radius.circular(numbers[0].toDouble()), - topRight: Radius.circular(numbers[1].toDouble()), - bottomLeft: Radius.circular(numbers[2].toDouble()), + topLeft: Radius.circular(numbers[0]), + topRight: Radius.circular(numbers[1]), + bottomLeft: Radius.circular(numbers[2]), ); case 4: return BorderRadius.only( - topLeft: Radius.circular(numbers[0].toDouble()), - topRight: Radius.circular(numbers[1].toDouble()), - bottomRight: Radius.circular(numbers[2].toDouble()), - bottomLeft: Radius.circular(numbers[3].toDouble()), + topLeft: Radius.circular(numbers[0]), + topRight: Radius.circular(numbers[1]), + bottomRight: Radius.circular(numbers[2]), + bottomLeft: Radius.circular(numbers[3]), ); default: - throw LanguageError('borderRadius requires 1 to 4 integers'); + throw LanguageError('borderRadius requires 1 to 4 numbers'); } } @@ -975,13 +975,13 @@ static BoxDecoration? getBoxDecoration(dynamic style) { } static EBorderRadius? getBorderRadius(dynamic value) { - if (value is int) { + if (value is num) { // optimize, ignore zero border radius as that causes extra processing for clipping if (value != 0) { - return EBorderRadius.all(value); + return EBorderRadius.all(value.toDouble()); } } else if (value is String) { - List numbers = stringToIntegers(value, min: 0); + List numbers = stringToDoubles(value, min: 0); if (numbers.length == 1) { return EBorderRadius.all(numbers[0]); } else if (numbers.length == 2) { @@ -992,7 +992,7 @@ static BoxDecoration? getBoxDecoration(dynamic style) { return EBorderRadius.only( numbers[0], numbers[1], numbers[2], numbers[3]); } else { - throw LanguageError('borderRadius requires 1 to 4 integers'); + throw LanguageError('borderRadius requires 1 to 4 numbers'); } } return null; @@ -1039,6 +1039,22 @@ static BoxDecoration? getBoxDecoration(dynamic style) { return rtn; } + /// Parse a whitespace-separated string into non-negative doubles. + static List stringToDoubles(String value, + {double? min, double? max}) { + List rtn = []; + + for (var val in value.split(RegExp(r'\s+'))) { + double? number = double.tryParse(val); + if (number != null && + (min == null || number >= min) && + (max == null || number <= max)) { + rtn.add(number); + } + } + return rtn; + } + static int? parseIntFromString(String value) { return int.tryParse(value); } diff --git a/modules/ensemble/lib/widget/checkbox.dart b/modules/ensemble/lib/widget/checkbox.dart index dec58c4f4..6c3995cec 100644 --- a/modules/ensemble/lib/widget/checkbox.dart +++ b/modules/ensemble/lib/widget/checkbox.dart @@ -148,9 +148,8 @@ class CheckboxState extends FormFieldWidgetState { Widget buildCheckbox(BuildContext context) { var theme = Theme.of(context); - int borderWidth = widget._controller.borderWidth ?? - theme.checkboxTheme.side?.width.toInt() ?? - 2; + double borderWidth = + widget._controller.borderWidth ?? theme.checkboxTheme.side?.width ?? 2; Color borderColor = widget._controller.borderColor ?? theme.checkboxTheme.side?.color ?? ThemeManager().getBorderColor(context); @@ -161,8 +160,7 @@ class CheckboxState extends FormFieldWidgetState { if (!states.contains(WidgetState.selected) && !states.contains(WidgetState.disabled) && !states.contains(WidgetState.error)) { - return BorderSide( - width: borderWidth.toDouble(), color: borderColor); + return BorderSide(width: borderWidth, color: borderColor); } // fallback to theme, which fallback to default return null; @@ -171,8 +169,7 @@ class CheckboxState extends FormFieldWidgetState { ? null : RoundedRectangleBorder( borderRadius: widget._controller.borderRadius!.getValue(), - side: BorderSide( - width: borderWidth.toDouble(), color: borderColor)), + side: BorderSide(width: borderWidth, color: borderColor)), activeColor: activeColor, checkColor: widget._controller.checkColor, fillColor: diff --git a/modules/ensemble/lib/widget/helpers/box_wrapper.dart b/modules/ensemble/lib/widget/helpers/box_wrapper.dart index f9c141bc4..05bfd11a4 100644 --- a/modules/ensemble/lib/widget/helpers/box_wrapper.dart +++ b/modules/ensemble/lib/widget/helpers/box_wrapper.dart @@ -66,7 +66,7 @@ class TVFocusStylingResolver { return tvOptions.focusBorderWidth ?? tvFocusTheme.focusBorderWidth ?? externalProvider?.focusBorderWidth ?? - boxController.borderWidth?.toDouble() ?? + boxController.borderWidth ?? TVFocusTheme.defaultBorderWidth; } @@ -96,14 +96,13 @@ BoxBorder? _resolveBoxBorder({ required bool hasBorder, required LinearGradient? borderGradient, required Color? borderColor, - required int? borderWidth, + required double? borderWidth, required BorderStyleComposite? borderStyle, required BuildContext context, }) { if (!hasBorder) return null; - final width = - borderWidth?.toDouble() ?? ThemeManager().getBorderThickness(context); + final width = borderWidth ?? ThemeManager().getBorderThickness(context); if (borderGradient != null) { return GradientBoxBorder(gradient: borderGradient, width: width); diff --git a/modules/ensemble/lib/widget/helpers/controllers.dart b/modules/ensemble/lib/widget/helpers/controllers.dart index 8e7d95fe0..b38d36dfe 100644 --- a/modules/ensemble/lib/widget/helpers/controllers.dart +++ b/modules/ensemble/lib/widget/helpers/controllers.dart @@ -943,7 +943,7 @@ class BoxController extends WidgetController { LinearGradient? backgroundGradient; Color? borderColor; - int? borderWidth; + double? borderWidth; EBorderRadius? borderRadius; LinearGradient? borderGradient; BorderStyleComposite? borderStyle; @@ -989,7 +989,7 @@ class BoxController extends WidgetController { borderGradient = Utils.getBackgroundGradient(value), 'borderColor': (value) => borderColor = Utils.getColor(value), - 'borderWidth': (value) => borderWidth = Utils.optionalInt(value), + 'borderWidth': (value) => borderWidth = Utils.optionalDouble(value), 'borderRadius': (value) => borderRadius = Utils.getBorderRadius(value), 'borderStyle': (value) => borderStyle = BorderStyleComposite.from(this, value), diff --git a/modules/ensemble/lib/widget/helpers/form_helper.dart b/modules/ensemble/lib/widget/helpers/form_helper.dart index 659a69d13..676d2a867 100644 --- a/modules/ensemble/lib/widget/helpers/form_helper.dart +++ b/modules/ensemble/lib/widget/helpers/form_helper.dart @@ -40,7 +40,7 @@ class FormFieldController extends WidgetController { Color? fillColor; EBorderRadius? borderRadius; - int? borderWidth; + double? borderWidth; Color? borderColor; Color? enabledBorderColor; @@ -77,7 +77,7 @@ class FormFieldController extends WidgetController { 'filled': (value) => filled = Utils.optionalBool(value), 'fillColor': (value) => fillColor = Utils.getColor(value), 'borderRadius': (value) => borderRadius = Utils.getBorderRadius(value), - 'borderWidth': (value) => borderWidth = Utils.optionalInt(value, min: 0), + 'borderWidth': (value) => borderWidth = Utils.optionalDouble(value, min: 0), 'borderColor': (color) => borderColor = Utils.getColor(color), 'enabledBorderColor': (color) => enabledBorderColor = Utils.getColor(color), @@ -169,12 +169,12 @@ abstract class FormFieldWidgetState InputVariant? variant = myController.variant ?? _themeVariant; // resolve borderWidth - int? _themeBorderWidth = themeDecoration.border?.borderSide.width.toInt(); + double? _themeBorderWidth = themeDecoration.border?.borderSide.width; if (myController.borderWidth != null && myController.borderWidth != _themeBorderWidth) { redrawAllBorders = true; } - int borderWidth = myController.borderWidth ?? _themeBorderWidth ?? 1; + double borderWidth = myController.borderWidth ?? _themeBorderWidth ?? 1; // resolve borderRadius BorderRadius? _themeBorderRadius = diff --git a/modules/ensemble/lib/widget/input/form_time.dart b/modules/ensemble/lib/widget/input/form_time.dart index 77ca148d2..1a72e9477 100644 --- a/modules/ensemble/lib/widget/input/form_time.dart +++ b/modules/ensemble/lib/widget/input/form_time.dart @@ -227,7 +227,7 @@ class AndroidTimePickerStyle { } class _BorderStyle { - final int borderWidth; + final double borderWidth; final BorderRadius borderRadius; final Color borderColor; @@ -328,8 +328,8 @@ class TimeState extends FormFieldWidgetState