diff --git a/CHANGELOG.md b/CHANGELOG.md index e89e562ed..9e2e3d770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ ## [next] - - refactor: Flutter 3.47.1 support - refactor: migrate Material UI imports to the standalone `material_ui` package - refactor: migrate Cupertino UI imports to the standalone `cupertino_ui` package - refactor: update localization delegates to use `GlobalMaterialLocalizations.delegates` with the new standalone UI packages - fix: skip hidden `TitleBar` children when collecting semantics ([#1354](https://github.com/bdlukaa/fluent_ui/pull/1354)) +- fix: `ComboBox` no longer throws a layout assertion when opened while fully aligned to the bottom edge of the viewport ([#1350](https://github.com/bdlukaa/fluent_ui/issues/1350)) ## 4.16.1 diff --git a/analysis_options.yaml b/analysis_options.yaml index 4e7b3470c..1ecf5f983 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,6 +8,13 @@ analyzer: - lib/src/styles/color_resources.dart - bin/** - "**/*.g.dart" + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** language: strict-casts: true strict-inference: true diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 1c7be75db..93a4a4ada 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -4,6 +4,13 @@ analyzer: exclude: - "**/*.g.dart" - "**/*.freezed.dart" + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** linter: rules: diff --git a/example/lib/routes/forms.dart b/example/lib/routes/forms.dart index edeb34210..d343176f5 100644 --- a/example/lib/routes/forms.dart +++ b/example/lib/routes/forms.dart @@ -2,7 +2,7 @@ export '../screens/forms/auto_suggest_box.dart'; export '../screens/forms/calendar_date_picker.dart'; export '../screens/forms/calendar_view.dart'; export '../screens/forms/color_picker.dart'; -export '../screens/forms/combobox.dart'; +export '../screens/forms/combo_box.dart'; export '../screens/forms/date_picker.dart'; export '../screens/forms/number_box.dart'; export '../screens/forms/password_box.dart'; diff --git a/example/lib/screens/forms/combobox.dart b/example/lib/screens/forms/combo_box.dart similarity index 100% rename from example/lib/screens/forms/combobox.dart rename to example/lib/screens/forms/combo_box.dart diff --git a/example/macos/Podfile b/example/macos/Podfile index 9ec46f8cd..0c76ccf54 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 9f5ca000f..1f752f4f9 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral SPEC CHECKSUMS: - FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + FlutterMacOS: c232990155153907050900a2e175c7773903ba4e -PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 +PODFILE CHECKSUM: 505596d150d38022472859d890f709281982e016 COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 6c1453bcc..6898ed47d 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -397,7 +397,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -476,7 +476,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -523,7 +523,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/lib/src/controls/flyouts/flyout.dart b/lib/src/controls/flyouts/flyout.dart index 25f24620c..c73daf57e 100644 --- a/lib/src/controls/flyouts/flyout.dart +++ b/lib/src/controls/flyouts/flyout.dart @@ -508,7 +508,8 @@ class _FlyoutPositionDelegate extends SingleChildLayoutDelegate { return clampDouble( y, margin, - (rootSize.height - flyoutSize.height - margin).clamp( + clampDouble( + rootSize.height - flyoutSize.height - margin, margin, rootSize.height - margin, ), diff --git a/lib/src/controls/flyouts/menu_bar.dart b/lib/src/controls/flyouts/menu_bar.dart index 2ff95b33f..fe7fe9ef2 100644 --- a/lib/src/controls/flyouts/menu_bar.dart +++ b/lib/src/controls/flyouts/menu_bar.dart @@ -268,8 +268,9 @@ class MenuBarState extends State { }, child: Container( constraints: BoxConstraints( - minHeight: (40 + theme.visualDensity.baseSizeAdjustment.dy).clamp( - 0.0, + minHeight: clampDouble( + 40 + theme.visualDensity.baseSizeAdjustment.dy, + 0, double.infinity, ), ), diff --git a/lib/src/controls/flyouts/tooltip.dart b/lib/src/controls/flyouts/tooltip.dart index 281a8062a..d8e45d24d 100644 --- a/lib/src/controls/flyouts/tooltip.dart +++ b/lib/src/controls/flyouts/tooltip.dart @@ -530,8 +530,9 @@ class TooltipThemeData with Diagnosticable { /// Creates the standard [TooltipThemeData] based on the given [theme]. factory TooltipThemeData.standard(FluentThemeData theme) { return TooltipThemeData( - height: (32 + theme.visualDensity.baseSizeAdjustment.dy).clamp( - 0.0, + height: clampDouble( + 32 + theme.visualDensity.baseSizeAdjustment.dy, + 0, double.infinity, ), verticalOffset: 24, diff --git a/lib/src/controls/form/auto_suggest_box.dart b/lib/src/controls/form/auto_suggest_box.dart index 525b4fc07..fbe3328b3 100644 --- a/lib/src/controls/form/auto_suggest_box.dart +++ b/lib/src/controls/form/auto_suggest_box.dart @@ -653,12 +653,16 @@ class AutoSuggestBoxState extends State> { final double overlayOffsetY; final double maxHeight; if (showAbove) { - maxHeight = spaceAbove.clamp(0.0, widget.maxPopupHeight); + maxHeight = clampDouble(spaceAbove, 0, widget.maxPopupHeight); overlayOffsetY = -(maxHeight + 0.8); } else { overlayOffsetY = box.size.height + 0.8; final overlayY = globalOffset.dy + box.size.height; - maxHeight = (screenHeight - overlayY).clamp(0.0, widget.maxPopupHeight); + maxHeight = clampDouble( + screenHeight - overlayY, + 0, + widget.maxPopupHeight, + ); } Widget child = PositionedDirectional( @@ -977,8 +981,9 @@ class _AutoSuggestBoxOverlayState extends State<_AutoSuggestBoxOverlay> { /// Returns the tile height adjusted for the given visual density. @visibleForTesting static double adjustedTileHeight(VisualDensity density) { - return (_tileHeight + density.baseSizeAdjustment.dy).clamp( - 0.0, + return clampDouble( + _tileHeight + density.baseSizeAdjustment.dy, + 0, double.infinity, ); } diff --git a/lib/src/controls/form/combo_box.dart b/lib/src/controls/form/combo_box.dart index 4bee05a66..95f26a526 100644 --- a/lib/src/controls/form/combo_box.dart +++ b/lib/src/controls/form/combo_box.dart @@ -63,12 +63,13 @@ class _ComboBoxMenuPainter extends CustomPainter { final maxTopOffset = math.max(0, size.height - scaledItemHeight); final minBottomOffset = math.min(scaledItemHeight, size.height); final top = Tween( - begin: selectedItemOffset.clamp(0.0, maxTopOffset), + begin: clampDouble(selectedItemOffset, 0, maxTopOffset), end: 0, ); final bottom = Tween( - begin: (top.begin! + scaledItemHeight).clamp( + begin: clampDouble( + top.begin! + scaledItemHeight, minBottomOffset, size.height, ), @@ -407,12 +408,13 @@ class _ComboBoxResizeClipper extends CustomClipper { final maxTopOffset = math.max(0, size.height - scaledItemHeight); final minBottomOffset = math.min(scaledItemHeight, size.height); final top = Tween( - begin: selectedItemOffset.clamp(0.0, maxTopOffset), + begin: clampDouble(selectedItemOffset, 0, maxTopOffset), end: 0, ); final bottom = Tween( - begin: (top.begin! + scaledItemHeight).clamp( + begin: clampDouble( + top.begin! + scaledItemHeight, minBottomOffset, size.height, ), @@ -472,6 +474,11 @@ class _ComboBoxMenuRouteLayout extends SingleChildLayoutDelegate { size.height, route.selectedIndex ?? 0, ); + final top = clampDouble( + menuLimits.top, + 0, + math.max(0, size.height - childSize.height), + ); assert(() { final container = Offset.zero & size; @@ -479,8 +486,8 @@ class _ComboBoxMenuRouteLayout extends SingleChildLayoutDelegate { // If the button was entirely on-screen, then verify // that the menu is also on-screen. // If the button was a bit off-screen, then, oh well. - assert(menuLimits.top >= 0.0); - assert(menuLimits.top + menuLimits.height <= size.height); + assert(top >= 0.0); + assert(top + childSize.height <= size.height); } return true; }()); @@ -488,12 +495,12 @@ class _ComboBoxMenuRouteLayout extends SingleChildLayoutDelegate { final double left; switch (textDirection!) { case TextDirection.rtl: - left = buttonRect.right.clamp(0.0, size.width) - childSize.width; + left = clampDouble(buttonRect.right, 0, size.width) - childSize.width; case TextDirection.ltr: - left = buttonRect.left.clamp(0.0, size.width - childSize.width); + left = clampDouble(buttonRect.left, 0, size.width - childSize.width); } - return Offset(left, menuLimits.top); + return Offset(left, top); } @override @@ -806,14 +813,15 @@ class _ComboBoxItemContainer extends StatelessWidget { final textScaler = MediaQuery.textScalerOf(context); final densityAdjustment = theme.visualDensity.baseSizeAdjustment.dy; - final adjustedItemHeight = - (textScaler.scale(kComboBoxItemHeight) + densityAdjustment).clamp( - 0.0, - double.infinity, - ); + final adjustedItemHeight = clampDouble( + textScaler.scale(kComboBoxItemHeight) + densityAdjustment, + 0, + double.infinity, + ); final itemHeight = adjustedItemHeight; - final buttonHeight = (adjustedItemHeight - _kMenuItemBottomPadding).clamp( - 0.0, + final buttonHeight = clampDouble( + adjustedItemHeight - _kMenuItemBottomPadding, + 0, double.infinity, ); return Container( diff --git a/lib/src/controls/form/number_box.dart b/lib/src/controls/form/number_box.dart index c3ff63223..df7188ece 100644 --- a/lib/src/controls/form/number_box.dart +++ b/lib/src/controls/form/number_box.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:ui' as ui; +import 'dart:ui'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; @@ -546,10 +547,12 @@ class NumberBoxState extends State> { if (boxContext == null) return const SizedBox.shrink(); final box = boxContext.findRenderObject()! as RenderBox; final isRtl = Directionality.of(context) == TextDirection.rtl; - final overlayHeight = - (kNumberBoxOverlayHeight + - FluentTheme.of(context).visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity); + final overlayHeight = clampDouble( + kNumberBoxOverlayHeight + + FluentTheme.of(context).visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ); final Widget child = PositionedDirectional( width: kNumberBoxOverlayWidth, @@ -913,9 +916,11 @@ class _NumberBoxCompactOverlay extends StatelessWidget { Widget build(BuildContext context) { assert(debugCheckHasFluentTheme(context)); final theme = FluentTheme.of(context); - final overlayHeight = - (kNumberBoxOverlayHeight + theme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity); + final overlayHeight = clampDouble( + kNumberBoxOverlayHeight + theme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ); return Padding( padding: const EdgeInsetsDirectional.only(start: 10), diff --git a/lib/src/controls/form/selection_controls.dart b/lib/src/controls/form/selection_controls.dart index 426fdc7da..1a6bcb3d2 100644 --- a/lib/src/controls/form/selection_controls.dart +++ b/lib/src/controls/form/selection_controls.dart @@ -411,7 +411,8 @@ class _WindowsTextSelectionControlsToolbarState assert(debugCheckHasMediaQuery(context)); final midpointAnchor = Offset( - (widget.selectionMidpoint.dx - widget.globalEditableRegion.left).clamp( + clampDouble( + widget.selectionMidpoint.dx - widget.globalEditableRegion.left, MediaQuery.paddingOf(context).left, MediaQuery.widthOf(context) - MediaQuery.paddingOf(context).right, ), diff --git a/lib/src/controls/form/text_box.dart b/lib/src/controls/form/text_box.dart index 04021a075..dde2c76f1 100644 --- a/lib/src/controls/form/text_box.dart +++ b/lib/src/controls/form/text_box.dart @@ -1609,9 +1609,11 @@ class _TextBoxState extends State child: Container( foregroundDecoration: foregroundDecoration, constraints: BoxConstraints( - minHeight: - (32 + themeData.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity), + minHeight: clampDouble( + 32 + themeData.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ), ), child: _selectionGestureDetectorBuilder .buildGestureDetector( diff --git a/lib/src/controls/inputs/rating.dart b/lib/src/controls/inputs/rating.dart index d0f8d6535..8225287fc 100644 --- a/lib/src/controls/inputs/rating.dart +++ b/lib/src/controls/inputs/rating.dart @@ -211,14 +211,14 @@ class _RatingControlState extends State { void increase() { if (widget.rating == widget.amount) return; widget.onChanged?.call( - (widget.rating + 1).clamp(0, widget.amount).toDouble(), + clampDouble(widget.rating + 1, 0, widget.amount.toDouble()), ); } void decrease() { if (widget.rating == 0) return; widget.onChanged?.call( - (widget.rating - 1).clamp(0, widget.amount).toDouble(), + clampDouble(widget.rating - 1, 0, widget.amount.toDouble()), ); } @@ -253,19 +253,20 @@ class _RatingControlState extends State { /// downstream calculations can assume left-to-right ordering. double _adjustX(double x) { if (Directionality.of(context) == TextDirection.rtl) { - return (_totalWidth - x).clamp(0.0, _totalWidth); + return clampDouble(_totalWidth - x, 0, _totalWidth); } return x; } void _handleUpdate(double x) { final totalPerStar = widget.iconSize + widget.starSpacing; - final raw = (_adjustX(x) / totalPerStar).clamp( - 0.0, + final raw = clampDouble( + _adjustX(x) / totalPerStar, + 0, widget.amount.toDouble(), ); - final snapped = raw.ceil().clamp(0, widget.amount).toDouble(); - widget.onChanged?.call(snapped); + final snapped = raw.ceil().clamp(0, widget.amount); + widget.onChanged?.call(snapped.toDouble()); } void _handleHoverUpdate(double x) { @@ -334,7 +335,7 @@ class _RatingControlState extends State { final double r; if (!isEnabled) { - r = (value - index).clamp(0.0, 1.0); + r = clampDouble(value - index, 0, 1); starRatedColor = widget.ratedIconColor ?? resources.textFillColorDisabled; @@ -363,7 +364,7 @@ class _RatingControlState extends State { } } else { // Normal (non-hover) state - r = (value - index).clamp(0.0, 1.0); + r = clampDouble(value - index, 0, 1); starRatedColor = widget.ratedIconColor ?? accentColor; // SelectedForeground diff --git a/lib/src/controls/inputs/toggle_switch.dart b/lib/src/controls/inputs/toggle_switch.dart index d8d31e79d..9311de2bc 100644 --- a/lib/src/controls/inputs/toggle_switch.dart +++ b/lib/src/controls/inputs/toggle_switch.dart @@ -182,7 +182,7 @@ class _ToggleSwitchState extends State { ) { setState(() { _alignment = Alignment( - (localPosition.dx / sliderGestureWidth).clamp(-1, 1), + clampDouble(localPosition.dx / sliderGestureWidth, -1, 1), 0, ); }); diff --git a/lib/src/controls/navigation/breadcrumb_bar.dart b/lib/src/controls/navigation/breadcrumb_bar.dart index 525213af7..50de1586b 100644 --- a/lib/src/controls/navigation/breadcrumb_bar.dart +++ b/lib/src/controls/navigation/breadcrumb_bar.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; /// The alignment of the chevron icon in the breadcrumb bar. @@ -541,7 +542,7 @@ class RenderBreadcrumbBar extends RenderBox onIndexOverflow(overflowedIndexes); size = Size( - maxExtent.clamp(constraints.maxWidth, constraints.maxWidth), + clampDouble(maxExtent, constraints.maxWidth, constraints.maxWidth), height, ); } diff --git a/lib/src/controls/navigation/navigation_view/indicators.dart b/lib/src/controls/navigation/navigation_view/indicators.dart index 207afa1aa..4bb8d58a9 100644 --- a/lib/src/controls/navigation/navigation_view/indicators.dart +++ b/lib/src/controls/navigation/navigation_view/indicators.dart @@ -380,8 +380,9 @@ class _StickyNavigationIndicatorState final fluentTheme = FluentTheme.of(context); final densityAdjustment = fluentTheme.visualDensity.baseSizeAdjustment.dy; - final paneItemMinHeight = (kPaneItemMinHeight + densityAdjustment).clamp( - 0.0, + final paneItemMinHeight = clampDouble( + kPaneItemMinHeight + densityAdjustment, + 0, double.infinity, ); @@ -455,8 +456,11 @@ class _StickyNavigationIndicatorState final expandProgress = expandAnimation.value; final tileHeight = constraints.maxHeight; final distanceToBottom = tileHeight - widget.leftPadding; - topPadding = (distanceToBottom * (1.0 - expandProgress)) - .clamp(widget.leftPadding, distanceToBottom); + topPadding = clampDouble( + distanceToBottom * (1.0 - expandProgress), + widget.leftPadding, + distanceToBottom, + ); } return Padding( diff --git a/lib/src/controls/navigation/navigation_view/pane.dart b/lib/src/controls/navigation/navigation_view/pane.dart index 4f569a013..f1ca927e1 100644 --- a/lib/src/controls/navigation/navigation_view/pane.dart +++ b/lib/src/controls/navigation/navigation_view/pane.dart @@ -547,7 +547,11 @@ class NavigationPaneSize with Diagnosticable { /// Gets the width of the open pane with the constraints applied. double get openPaneWidth { - return openWidth.clamp(openMinWidth ?? 0, openMaxWidth ?? double.infinity); + return clampDouble( + openWidth, + openMinWidth ?? 0, + openMaxWidth ?? double.infinity, + ); } @override diff --git a/lib/src/controls/navigation/navigation_view/pane_items.dart b/lib/src/controls/navigation/navigation_view/pane_items.dart index 36b3d86f0..f7d923ca4 100644 --- a/lib/src/controls/navigation/navigation_view/pane_items.dart +++ b/lib/src/controls/navigation/navigation_view/pane_items.dart @@ -205,8 +205,9 @@ class PaneItem extends NavigationPaneItem { final theme = NavigationPaneTheme.of(context); final fluentTheme = FluentTheme.of(context); final densityAdjustment = fluentTheme.visualDensity.baseSizeAdjustment.dy; - final paneItemMinHeight = (kPaneItemMinHeight + densityAdjustment).clamp( - 0.0, + final paneItemMinHeight = clampDouble( + kPaneItemMinHeight + densityAdjustment, + 0, double.infinity, ); diff --git a/lib/src/controls/navigation/navigation_view/title_bar.dart b/lib/src/controls/navigation/navigation_view/title_bar.dart index 6a0277186..08726051c 100644 --- a/lib/src/controls/navigation/navigation_view/title_bar.dart +++ b/lib/src/controls/navigation/navigation_view/title_bar.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; /// The title bar sits at the top of an app on the base layer. Its main purpose @@ -623,9 +624,11 @@ class PaneToggleButton extends StatelessWidget { Widget build(BuildContext context) { final view = NavigationView.dataOf(context); final fluentTheme = FluentTheme.of(context); - final paneItemHeight = - (kPaneItemMinHeight + fluentTheme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity); + final paneItemHeight = clampDouble( + kPaneItemMinHeight + fluentTheme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ); final width = view.pane?.size?.compactWidth ?? kCompactNavigationPaneWidth; return Container( @@ -682,9 +685,11 @@ class PaneBackButton extends StatelessWidget { final viewData = NavigationView.dataOf(context); final canPop = viewData.canPop; final fluentTheme = FluentTheme.of(context); - final paneItemHeight = - (kPaneItemMinHeight + fluentTheme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity); + final paneItemHeight = clampDouble( + kPaneItemMinHeight + fluentTheme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ); final width = viewData.pane?.size?.compactWidth ?? kCompactNavigationPaneWidth; diff --git a/lib/src/controls/navigation/tab_view/tab_view.dart b/lib/src/controls/navigation/tab_view/tab_view.dart index cb3350c45..b20db6548 100644 --- a/lib/src/controls/navigation/tab_view/tab_view.dart +++ b/lib/src/controls/navigation/tab_view/tab_view.dart @@ -500,14 +500,14 @@ class _TabViewState extends State { 'You can only create a TabView in a box with defined width', ); - preferredTabWidth = - ((width - - (widget.showNewButton - ? _kButtonWidth - : 0) - - (widget.reservedStripWidth ?? 0)) / - widget.tabs.length) - .clamp(widget.minTabWidth, widget.maxTabWidth); + preferredTabWidth = clampDouble( + (width - + (widget.showNewButton ? _kButtonWidth : 0) - + (widget.reservedStripWidth ?? 0)) / + widget.tabs.length, + widget.minTabWidth, + widget.maxTabWidth, + ); final Widget listView = Listener( onPointerSignal: (e) { diff --git a/lib/src/controls/navigation/tree_view.dart b/lib/src/controls/navigation/tree_view.dart index 573706344..340ee0c34 100644 --- a/lib/src/controls/navigation/tree_view.dart +++ b/lib/src/controls/navigation/tree_view.dart @@ -1434,12 +1434,14 @@ class _TreeViewItem extends StatelessWidget { // Indentation and selection indicator for single selection mode. Container( constraints: BoxConstraints( - minHeight: - ((selectionMode == TreeViewSelectionMode.multiple - ? 28.0 - : 26.0) + - theme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity), + minHeight: clampDouble( + (selectionMode == TreeViewSelectionMode.multiple + ? 28.0 + : 26.0) + + theme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ), ), padding: EdgeInsetsDirectional.only( start: selectionMode == TreeViewSelectionMode.multiple diff --git a/lib/src/controls/pickers/color_picker/color_spectrum.dart b/lib/src/controls/pickers/color_picker/color_spectrum.dart index b885188f4..58edbee7b 100644 --- a/lib/src/controls/pickers/color_picker/color_spectrum.dart +++ b/lib/src/controls/pickers/color_picker/color_spectrum.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'dart:ui' as dart; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'color_state.dart'; @@ -273,8 +274,8 @@ class _ColorBoxSpectrumState extends State { final height = size.height; // Clamp position within bounds - final x = position.dx.clamp(0, width); - final y = position.dy.clamp(0, height); + final x = clampDouble(position.dx, 0, width); + final y = clampDouble(position.dy, 0, height); // Calculate HSV values // Hue from left to right (minHue to maxHue) @@ -461,12 +462,16 @@ class _RingSpectrumPainter extends CustomPainter { final labelWidth = textPainter.width + labelPadding.horizontal; final labelHeight = textPainter.height + labelPadding.vertical; - final labelX = (position.dx - labelWidth / 2) - .clamp(0, size.width - labelWidth) - .toDouble(); - var labelY = (position.dy - labelHeight - 30) - .clamp(0, size.height - labelHeight) - .toDouble(); + final labelX = clampDouble( + position.dx - labelWidth / 2, + 0, + size.width - labelWidth, + ); + var labelY = clampDouble( + position.dy - labelHeight - 30, + 0, + size.height - labelHeight, + ); // Check if label would overlap the indicator and adjust position final labelBottomY = labelY + labelHeight; @@ -676,12 +681,16 @@ class _BoxSpectrumPainter extends CustomPainter { final labelWidth = textPainter.width + labelPadding.horizontal; final labelHeight = textPainter.height + labelPadding.vertical; - final labelX = (position.dx - labelWidth / 2) - .clamp(0, size.width - labelWidth) - .toDouble(); - var labelY = (position.dy - labelHeight - 30) - .clamp(0, size.height - labelHeight) - .toDouble(); + final labelX = clampDouble( + position.dx - labelWidth / 2, + 0, + size.width - labelWidth, + ); + var labelY = clampDouble( + position.dy - labelHeight - 30, + 0, + size.height - labelHeight, + ); // Check if label would overlap the indicator and adjust position final labelBottomY = labelY + labelHeight; diff --git a/lib/src/controls/pickers/color_picker/color_state.dart b/lib/src/controls/pickers/color_picker/color_state.dart index cce16a1e6..efdfa23a7 100644 --- a/lib/src/controls/pickers/color_picker/color_state.dart +++ b/lib/src/controls/pickers/color_picker/color_state.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; part 'color_names.dart'; @@ -120,14 +121,14 @@ class ColorState extends ChangeNotifier { /// Sets the saturation and updates the RGB values accordingly. void setSaturation(double newValue) { - _saturation = newValue.clamp(0, 1); + _saturation = clampDouble(newValue, 0, 1); _recalculateRGBFromHSV(); notifyListeners(); } /// Sets the value and updates the RGB values accordingly. void setValue(double newValue) { - _value = newValue.clamp(0, 1); + _value = clampDouble(newValue, 0, 1); if (_value == 0) { _red = 0; @@ -145,28 +146,28 @@ class ColorState extends ChangeNotifier { /// Sets the red component and updates the HSV values accordingly. void setRed(double newValue) { - _red = newValue.clamp(0, 1); + _red = clampDouble(newValue, 0, 1); _recalculateHSVFromRGB(); notifyListeners(); } /// Sets the green component and updates the HSV values accordingly. void setGreen(double newValue) { - _green = newValue.clamp(0, 1); + _green = clampDouble(newValue, 0, 1); _recalculateHSVFromRGB(); notifyListeners(); } /// Sets the blue component and updates the HSV values accordingly. void setBlue(double newValue) { - _blue = newValue.clamp(0, 1); + _blue = clampDouble(newValue, 0, 1); _recalculateHSVFromRGB(); notifyListeners(); } /// Sets the alpha component (0–1). void setAlpha(double newValue) { - _alpha = newValue.clamp(0, 1); + _alpha = clampDouble(newValue, 0, 1); notifyListeners(); } @@ -286,12 +287,13 @@ class ColorState extends ChangeNotifier { ); // Clamp values to allowed ranges - final clampedHue = _hue.clamp(minHue.toDouble(), maxHue.toDouble()); - final clampedSaturation = _saturation.clamp( + final clampedHue = clampDouble(_hue, minHue.toDouble(), maxHue.toDouble()); + final clampedSaturation = clampDouble( + _saturation, minSaturation / 100, maxSaturation / 100, ); - final clampedValue = _value.clamp(minValue / 100, maxValue / 100); + final clampedValue = clampDouble(_value, minValue / 100, maxValue / 100); // Only update and recalculate if values actually changed if (clampedHue != _hue || diff --git a/lib/src/controls/pickers/date_picker.dart b/lib/src/controls/pickers/date_picker.dart index 422ce82d0..2455de646 100644 --- a/lib/src/controls/pickers/date_picker.dart +++ b/lib/src/controls/pickers/date_picker.dart @@ -451,9 +451,11 @@ class DatePickerState extends State { duration: theme.fastAnimationDuration, curve: theme.animationCurve, constraints: BoxConstraints( - minHeight: - (kPickerHeight + theme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity), + minHeight: clampDouble( + kPickerHeight + theme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ), ), decoration: kPickerDecorationBuilder(context, states), child: DefaultTextStyle.merge( diff --git a/lib/src/controls/pickers/pickers.dart b/lib/src/controls/pickers/pickers.dart index 04940a227..1e63020d5 100644 --- a/lib/src/controls/pickers/pickers.dart +++ b/lib/src/controls/pickers/pickers.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; /// The padding used on the content of [DatePicker] and [TimePicker] @@ -395,7 +396,7 @@ class PickerState extends State { y = 0; } - y = y.clamp(0.0, rootHeight); + y = clampDouble(y, 0.0, rootHeight); final theme = FluentTheme.of(context); diff --git a/lib/src/controls/pickers/time_picker.dart b/lib/src/controls/pickers/time_picker.dart index 591ce0846..7c0653aff 100644 --- a/lib/src/controls/pickers/time_picker.dart +++ b/lib/src/controls/pickers/time_picker.dart @@ -318,9 +318,11 @@ class TimePickerState extends State duration: theme.fastAnimationDuration, curve: theme.animationCurve, constraints: BoxConstraints( - minHeight: - (kPickerHeight + theme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity), + minHeight: clampDouble( + kPickerHeight + theme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ), ), decoration: kPickerDecorationBuilder(context, states), child: DefaultTextStyle.merge( diff --git a/lib/src/controls/surfaces/acrylic.dart b/lib/src/controls/surfaces/acrylic.dart index 3da353d40..6d4f029c1 100644 --- a/lib/src/controls/surfaces/acrylic.dart +++ b/lib/src/controls/surfaces/acrylic.dart @@ -573,7 +573,7 @@ class AcrylicHelper { static Color getLuminosityColor(Color tintColor, double? luminosityOpacity) { // If luminosity opacity is specified, just use the values as is if (luminosityOpacity != null) { - return tintColor.withValues(alpha: luminosityOpacity.clamp(0.0, 1.0)); + return tintColor.withValues(alpha: clampDouble(luminosityOpacity, 0, 1)); } else { // To create the Luminosity blend input color without luminosity opacity, // we're taking the TintColor input, converting to HSV, and clamping the V between these values @@ -582,7 +582,7 @@ class AcrylicHelper { final hsvTintColor = HSVColor.fromColor(tintColor); - final clampedHsvV = hsvTintColor.value.clamp(minHsvV, maxHsvV); + final clampedHsvV = clampDouble(hsvTintColor.value, minHsvV, maxHsvV); final hsvLuminosityColor = hsvTintColor.withValue(clampedHsvV); final rgbLuminosityColor = hsvLuminosityColor.toColor(); diff --git a/lib/src/controls/surfaces/expander.dart b/lib/src/controls/surfaces/expander.dart index cb5a7892f..412b9eadc 100644 --- a/lib/src/controls/surfaces/expander.dart +++ b/lib/src/controls/surfaces/expander.dart @@ -319,8 +319,9 @@ class ExpanderState extends State builder: (context, states) { return Container( constraints: BoxConstraints( - minHeight: (42 + theme.visualDensity.baseSizeAdjustment.dy).clamp( - 0.0, + minHeight: clampDouble( + 42 + theme.visualDensity.baseSizeAdjustment.dy, + 0, double.infinity, ), ), diff --git a/lib/src/controls/surfaces/info_bar.dart b/lib/src/controls/surfaces/info_bar.dart index 64e90a439..c7e8f155a 100644 --- a/lib/src/controls/surfaces/info_bar.dart +++ b/lib/src/controls/surfaces/info_bar.dart @@ -397,8 +397,9 @@ class InfoBar extends StatelessWidget { }(); return Container( constraints: BoxConstraints( - minHeight: (48 + theme.visualDensity.baseSizeAdjustment.dy).clamp( - 0.0, + minHeight: clampDouble( + 48 + theme.visualDensity.baseSizeAdjustment.dy, + 0, double.infinity, ), ), diff --git a/lib/src/controls/surfaces/list_tile.dart b/lib/src/controls/surfaces/list_tile.dart index 21b69609d..22cec0795 100644 --- a/lib/src/controls/surfaces/list_tile.dart +++ b/lib/src/controls/surfaces/list_tile.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; /// The default height of a one-line list tile. @@ -329,10 +330,12 @@ class ListTile extends StatelessWidget { child: Container( decoration: ShapeDecoration(shape: shape, color: tileColor), constraints: BoxConstraints( - minHeight: - (kOneLineTileHeight + - theme.visualDensity.baseSizeAdjustment.dy) - .clamp(0.0, double.infinity), + minHeight: clampDouble( + kOneLineTileHeight + + theme.visualDensity.baseSizeAdjustment.dy, + 0, + double.infinity, + ), minWidth: 88, ), margin: margin, diff --git a/lib/src/controls/surfaces/progress_indicators.dart b/lib/src/controls/surfaces/progress_indicators.dart index 7944063f6..32f8e7b65 100644 --- a/lib/src/controls/surfaces/progress_indicators.dart +++ b/lib/src/controls/surfaces/progress_indicators.dart @@ -260,7 +260,7 @@ class _ProgressBarPainter extends CustomPainter { if (value != null) { drawLine( Offset(0, size.height), - Offset(value!.clamp(0.0, 1.0) * size.width, size.height), + Offset(clampDouble(value!, 0, 1) * size.width, size.height), activeColor, ); return; @@ -559,7 +559,7 @@ class _RingPainter extends CustomPainter { canvas.drawArc( offset & size, _startAngle, - (value! / 100).clamp(0, 1) * _sweep, + clampDouble(value! / 100, 0, 1) * _sweep, false, paint, ); diff --git a/lib/src/controls/utils/custom_icons.dart b/lib/src/controls/utils/custom_icons.dart index 4e4cae3ec..6c17e6d82 100644 --- a/lib/src/controls/utils/custom_icons.dart +++ b/lib/src/controls/utils/custom_icons.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; /// The chevron down icon. /// @@ -41,7 +42,7 @@ class ChevronDown extends StatelessWidget { child: Icon(icon, size: iconSize, color: iconColor), builder: (context, value, child) { return Opacity( - opacity: value.clamp(0.0, 1.0), + opacity: clampDouble(value, 0.0, 1.0), child: Transform.translate( filterQuality: FilterQuality.high, offset: Offset(0, value == 1 ? 0 : value * 1), diff --git a/lib/src/utils.dart b/lib/src/utils.dart index cb3bb25a6..7ba70f267 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -143,7 +143,11 @@ Offset horizontalPositionDependentBox({ if (size.height - margin * 2.0 < childSize.height) { y = (size.height - childSize.height) / 2.0; } else { - final normalizedTargetY = target.dy.clamp(margin, size.height - margin); + final normalizedTargetY = clampDouble( + target.dy, + margin, + size.height - margin, + ); final edge = margin + childSize.height / 2.0; if (normalizedTargetY < edge) { y = margin; diff --git a/test/combo_box_test.dart b/test/combo_box_test.dart index 03d7825ad..200816af7 100644 --- a/test/combo_box_test.dart +++ b/test/combo_box_test.dart @@ -315,4 +315,50 @@ void main() { expect(selectedValue, 'something'); }); + + testWidgets('ComboBox positioned at bottom opens without layout assertions', ( + tester, + ) async { + const selectedValue = 'Two'; + + await tester.pumpWidget( + wrapApp( + child: SizedBox( + height: 602, + child: Align( + alignment: Alignment.bottomCenter, + child: ComboBox( + value: selectedValue, + items: const [ + ComboBoxItem( + key: Key('bottom-combo-item-one'), + value: 'One', + child: Text('One'), + ), + ComboBoxItem( + key: Key('bottom-combo-item-two'), + value: 'Two', + child: Text('Two'), + ), + ComboBoxItem( + key: Key('bottom-combo-item-three'), + value: 'Three', + child: Text('Three'), + ), + ], + onChanged: (_) {}, + ), + ), + ), + ), + ); + + await tester.tap(find.byType(ComboBox)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.byKey(const Key('bottom-combo-item-one')), findsWidgets); + expect(find.byKey(const Key('bottom-combo-item-two')), findsWidgets); + expect(find.byKey(const Key('bottom-combo-item-three')), findsWidgets); + }); }