diff --git a/CHANGELOG.md b/CHANGELOG.md index f226aca81..833492e82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,14 @@ Date format: DD/MM/YYYY - `DisableAcrylic` now fully disable transparency of its decendents `Acrylic`s ([#468](/~https://github.com/bdlukaa/fluent_ui/issues/468)) - `Combobox.comboboxColor` is now correctly applied ([#468](/~https://github.com/bdlukaa/fluent_ui/issues/468)) -- Use the correct color for `DefaultToggleSwitchThumb` ([#463](/~https://github.com/bdlukaa/fluent_ui/issues/463)) - Do not interpolate between infinite constraints on `TabView` ([#430](/~https://github.com/bdlukaa/fluent_ui/issues/430)) - Do not rebuild the `TimePicker` popup when already rebuilding ([#437](/~https://github.com/bdlukaa/fluent_ui/issues/437)) +- `ToggleSwitch` updates: + - Use the correct color for `DefaultToggleSwitchThumb` ([#463](/~https://github.com/bdlukaa/fluent_ui/issues/463)) + - Added `ToggleSwitch.leadingContent`, which positions the content before the switch ([#464](/~https://github.com/bdlukaa/fluent_ui/issues/464)) + - Added `ToggleSwitch.thumbBuilder`, which builds the thumb based on the current state +- Added `TextChangedReason.cleared`, which is called when the text is cleared by the user in an `AutoSuggestBox` ([#461](/~https://github.com/bdlukaa/fluent_ui/issues/461)) +- `Tooltip` overlay is now ignored when hovered ([#443](/~https://github.com/bdlukaa/fluent_ui/issues/443)) ## [4.0.0-pre.3] - Top navigation and auto suggestions - [13/08/2022] diff --git a/example/lib/screens/inputs/button.dart b/example/lib/screens/inputs/button.dart index afecf2983..f78235350 100644 --- a/example/lib/screens/inputs/button.dart +++ b/example/lib/screens/inputs/button.dart @@ -6,16 +6,14 @@ import 'package:fluent_ui/fluent_ui.dart'; import '../../widgets/card_highlight.dart'; class ButtonPage extends ScrollablePage { - PageState state = { - 'simple_disabled': false, - 'filled_disabled': false, - 'icon_disabled': false, - 'toggle_state': false, - 'toggle_disabled': false, - 'split_button_disabled': false, - 'radio_button_disabled': false, - 'radio_button_selected': -1, - }; + bool simpleDisabled = false; + bool filledDisabled = false; + bool iconDisabled = false; + bool toggleDisabled = false; + bool toggleState = false; + bool splitButtonDisabled = false; + bool radioButtonDisabled = false; + int radioButtonSelected = -1; @override Widget buildHeader(BuildContext context) { @@ -33,14 +31,14 @@ class ButtonPage extends ScrollablePage { child: Row(children: [ Button( child: const Text('Standart Button'), - onPressed: state['simple_disabled'] ? null : () {}, + onPressed: simpleDisabled ? null : () {}, ), const Spacer(), ToggleSwitch( - checked: state['simple_disabled'], + checked: simpleDisabled, onChanged: (v) { setState(() { - state['simple_disabled'] = v; + simpleDisabled = v; }); }, content: const Text('Disabled'), @@ -56,14 +54,14 @@ class ButtonPage extends ScrollablePage { child: Row(children: [ FilledButton( child: const Text('Filled Button'), - onPressed: state['filled_disabled'] ? null : () {}, + onPressed: filledDisabled ? null : () {}, ), const Spacer(), ToggleSwitch( - checked: state['filled_disabled'], + checked: filledDisabled, onChanged: (v) { setState(() { - state['filled_disabled'] = v; + filledDisabled = v; }); }, content: const Text('Disabled'), @@ -81,14 +79,14 @@ class ButtonPage extends ScrollablePage { child: Row(children: [ IconButton( icon: const Icon(FluentIcons.graph_symbol, size: 24.0), - onPressed: state['icon_disabled'] ? null : () {}, + onPressed: iconDisabled ? null : () {}, ), const Spacer(), ToggleSwitch( - checked: state['icon_disabled'], + checked: iconDisabled, onChanged: (v) { setState(() { - state['icon_disabled'] = v; + iconDisabled = v; }); }, content: const Text('Disabled'), @@ -107,21 +105,21 @@ class ButtonPage extends ScrollablePage { child: Row(children: [ ToggleButton( child: const Text('Toggle Button'), - checked: state['toggle_state'], - onChanged: state['toggle_disabled'] + checked: toggleState, + onChanged: toggleDisabled ? null : (v) { setState(() { - state['toggle_state'] = v; + toggleState = v; }); }, ), const Spacer(), ToggleSwitch( - checked: state['toggle_disabled'], + checked: toggleDisabled, onChanged: (v) { setState(() { - state['toggle_disabled'] = v; + toggleDisabled = v; }); }, content: const Text('Disabled'), @@ -189,7 +187,7 @@ ToggleButton( Button( child: Container( decoration: BoxDecoration( - color: state['split_button_disabled'] + color: splitButtonDisabled ? FluentTheme.of(context).accentColor.darker : FluentTheme.of(context).accentColor, borderRadius: const BorderRadiusDirectional.horizontal( @@ -199,23 +197,23 @@ ToggleButton( height: 24, width: 24, ), - onPressed: state['split_button_disabled'] ? null : () {}, + onPressed: splitButtonDisabled ? null : () {}, ), IconButton( icon: const SizedBox( // height: splitButtonHeight, child: Icon(FluentIcons.chevron_down, size: 10.0), ), - onPressed: state['split_button_disabled'] ? null : () {}, + onPressed: splitButtonDisabled ? null : () {}, ), ], ), const Spacer(), ToggleSwitch( - checked: state['split_button_disabled'], + checked: splitButtonDisabled, onChanged: (v) { setState(() { - state['split_button_disabled'] = v; + splitButtonDisabled = v; }); }, content: const Text('Disabled'), @@ -253,13 +251,13 @@ ToggleButton( return Padding( padding: EdgeInsets.only(bottom: index == 2 ? 0.0 : 14.0), child: RadioButton( - checked: state['radio_button_selected'] == index, - onChanged: state['radio_button_disabled'] + checked: radioButtonSelected == index, + onChanged: radioButtonDisabled ? null : (v) { if (v) { setState(() { - state['radio_button_selected'] = index; + radioButtonSelected = index; }); } }, @@ -271,10 +269,10 @@ ToggleButton( ), const Spacer(), ToggleSwitch( - checked: state['radio_button_disabled'], + checked: radioButtonDisabled, onChanged: (v) { setState(() { - state['radio_button_disabled'] = v; + radioButtonDisabled = v; }); }, content: const Text('Disabled'), diff --git a/example/lib/screens/inputs/checkbox.dart b/example/lib/screens/inputs/checkbox.dart index ba2071e30..7621752b0 100644 --- a/example/lib/screens/inputs/checkbox.dart +++ b/example/lib/screens/inputs/checkbox.dart @@ -3,13 +3,11 @@ import 'package:example/widgets/page.dart'; import 'package:fluent_ui/fluent_ui.dart'; class CheckboxPage extends ScrollablePage { - PageState state = { - 'first_checked': false, - 'first_disabled': false, - 'second_state': false, - 'second_disabled': false, - 'icon_disabled': false, - }; + bool firstChecked = false; + bool firstDisabled = false; + bool? secondChecked = false; + bool secondDisabled = false; + bool iconDisabled = false; @override Widget buildHeader(BuildContext context) { @@ -26,22 +24,22 @@ class CheckboxPage extends ScrollablePage { CardHighlight( child: Row(children: [ Checkbox( - checked: state['first_checked'], - onChanged: state['first_disabled'] + checked: firstChecked, + onChanged: firstDisabled ? null : (v) { setState(() { - state['first_checked'] = v; + firstChecked = v!; }); }, content: const Text('Two-state Checkbox'), ), const Spacer(), ToggleSwitch( - checked: state['first_disabled'], + checked: firstDisabled, onChanged: (v) { setState(() { - state['first_disabled'] = v; + firstDisabled = v; }); }, content: const Text('Disabled'), @@ -58,13 +56,13 @@ Checkbox( CardHighlight( child: Row(children: [ Checkbox( - checked: state['second_state'], + checked: secondChecked, // checked: null, - onChanged: state['second_disabled'] + onChanged: secondDisabled ? null : (v) { setState(() { - state['second_state'] = v == true + secondChecked = v == true ? true : v == false ? null @@ -77,10 +75,10 @@ Checkbox( ), const Spacer(), ToggleSwitch( - checked: state['second_disabled'], + checked: secondDisabled, onChanged: (v) { setState(() { - state['second_disabled'] = v; + secondDisabled = v; }); }, content: const Text('Disabled'), diff --git a/example/lib/screens/inputs/slider.dart b/example/lib/screens/inputs/slider.dart index a55cdd700..6956e1467 100644 --- a/example/lib/screens/inputs/slider.dart +++ b/example/lib/screens/inputs/slider.dart @@ -3,26 +3,22 @@ import 'package:example/widgets/page.dart'; import 'package:fluent_ui/fluent_ui.dart'; class SliderPage extends ScrollablePage { - PageState state = { - 'disabled': false, - 'first_value': 23.0, - 'vertical_value': 50.0, - }; + bool disabled = false; + double firstValue = 23.0; + double verticalValue = 50.0; @override Widget buildHeader(BuildContext context) { return PageHeader( title: const Text('Slider'), commandBar: ToggleSwitch( - checked: isDisabled, - onChanged: (v) => setState(() => state['disabled'] = v), + checked: disabled, + onChanged: (v) => setState(() => disabled = v), content: const Text('Disabled'), ), ); } - bool get isDisabled => state['disabled']; - @override List buildScrollable(BuildContext context) { return [ @@ -32,16 +28,16 @@ class SliderPage extends ScrollablePage { CardHighlight( child: Row(children: [ Slider( - label: '${state['first_value'].toInt()}', - value: state['first_value'], - onChanged: isDisabled + label: '${firstValue.toInt()}', + value: firstValue, + onChanged: disabled ? null : (v) { - setState(() => state['first_value'] = v); + setState(() => firstValue = v); }, ), const Spacer(), - Text('Output:\n${state['first_value'].toInt()}'), + Text('Output:\n${firstValue.toInt()}'), ]), codeSnippet: '''double value = 0; @@ -57,16 +53,13 @@ Slider( child: Row(children: [ Slider( vertical: true, - label: '${state['vertical_value'].toInt()}', - value: state['vertical_value'], - onChanged: isDisabled - ? null - : (v) { - setState(() => state['vertical_value'] = v); - }, + label: '${verticalValue.toInt()}', + value: verticalValue, + onChanged: + disabled ? null : (v) => setState(() => verticalValue = v), ), const Spacer(), - Text('Output:\n${state['vertical_value'].toInt()}'), + Text('Output:\n${verticalValue.toInt()}'), ]), codeSnippet: '''double value = 0; diff --git a/example/lib/screens/inputs/toggle_switch.dart b/example/lib/screens/inputs/toggle_switch.dart index a7dd23b7e..9a87dd218 100644 --- a/example/lib/screens/inputs/toggle_switch.dart +++ b/example/lib/screens/inputs/toggle_switch.dart @@ -3,26 +3,22 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:example/widgets/card_highlight.dart'; class ToggleSwitchPage extends ScrollablePage { - PageState state = { - 'disabled': false, - 'first_value': false, - 'second_value': true, - }; + bool disabled = false; + bool firstValue = false; + bool secondValue = true; @override Widget buildHeader(BuildContext context) { return PageHeader( title: const Text('ToggleSwitch'), commandBar: ToggleSwitch( - checked: isDisabled, - onChanged: (v) => setState(() => state['disabled'] = v), + checked: disabled, + onChanged: (v) => setState(() => disabled = v), content: const Text('Disabled'), ), ); } - bool get isDisabled => state['disabled']; - @override List buildScrollable(BuildContext context) { return [ @@ -34,13 +30,13 @@ class ToggleSwitchPage extends ScrollablePage { child: Align( alignment: Alignment.centerLeft, child: ToggleSwitch( - checked: state['first_value'], - onChanged: isDisabled + checked: firstValue, + onChanged: disabled ? null : (v) { - setState(() => state['first_value'] = v); + setState(() => firstValue = v); }, - content: Text(state['first_value'] ? 'On' : 'Off'), + content: Text(firstValue ? 'On' : 'Off'), ), ), codeSnippet: '''bool checked = false; @@ -58,16 +54,16 @@ ToggleSwitch( InfoLabel( label: 'Header', child: ToggleSwitch( - checked: state['second_value'], - onChanged: isDisabled + checked: secondValue, + onChanged: disabled ? null : (v) { - setState(() => state['second_value'] = v); + setState(() => secondValue = v); }, - content: Text(state['second_value'] ? 'Working' : 'Do work'), + content: Text(secondValue ? 'Working' : 'Do work'), ), ), - if (state['second_value']) + if (secondValue) const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: ProgressRing(), diff --git a/example/lib/widgets/page.dart b/example/lib/widgets/page.dart index 6abc8f6b4..4bc4ff1af 100644 --- a/example/lib/widgets/page.dart +++ b/example/lib/widgets/page.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'package:fluent_ui/fluent_ui.dart'; -typedef PageState = Map; - abstract class Page { Page() { _pageIndex++; diff --git a/lib/src/controls/form/auto_suggest_box.dart b/lib/src/controls/form/auto_suggest_box.dart index a0a6a431c..d6634d7df 100644 --- a/lib/src/controls/form/auto_suggest_box.dart +++ b/lib/src/controls/form/auto_suggest_box.dart @@ -23,6 +23,9 @@ enum TextChangedReason { /// Whether the text in an [AutoSuggestBox] was changed because the user /// chose the suggestion suggestionChosen, + + /// Whether the text in an [AutoSuggestBox] was cleared by the user + cleared, } /// An item used in [AutoSuggestBox] @@ -545,6 +548,10 @@ class _AutoSuggestBoxState extends State { icon: const Icon(FluentIcons.chrome_close), onPressed: () { controller.clear(); + widget.onChanged?.call( + controller.text, + TextChangedReason.cleared, + ); focusNode.unfocus(); }, ), diff --git a/lib/src/controls/inputs/checkbox.dart b/lib/src/controls/inputs/checkbox.dart index b34a44812..55eec5a38 100644 --- a/lib/src/controls/inputs/checkbox.dart +++ b/lib/src/controls/inputs/checkbox.dart @@ -116,7 +116,7 @@ class Checkbox extends StatelessWidget { style.checkedIconColor?.resolve(state) ?? FluentTheme.of(context).inactiveColor, ) - : Icon( + : _Icon( style.icon, size: 12, color: () { @@ -371,3 +371,94 @@ class CheckboxThemeData with Diagnosticable { properties.add(DiagnosticsProperty('margin', margin)); } } + +/// Copy if [Icon], with specified font weight +/// See /~https://github.com/bdlukaa/fluent_ui/issues/471 +class _Icon extends StatelessWidget { + const _Icon( + this.icon, { + Key? key, + this.size, + this.color, + }) : super(key: key); + + final IconData? icon; + + final double? size; + + final Color? color; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasDirectionality(context)); + final TextDirection textDirection = Directionality.of(context); + + final IconThemeData iconTheme = IconTheme.of(context); + + final double? iconSize = size ?? iconTheme.size; + + final List? iconShadows = iconTheme.shadows; + + if (icon == null) { + return SizedBox(width: iconSize, height: iconSize); + } + + final double iconOpacity = iconTheme.opacity ?? 1.0; + Color iconColor = color ?? iconTheme.color!; + if (iconOpacity != 1.0) { + iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity); + } + + Widget iconWidget = RichText( + overflow: TextOverflow.visible, // Never clip. + textDirection: + textDirection, // Since we already fetched it for the assert... + text: TextSpan( + text: String.fromCharCode(icon!.codePoint), + style: TextStyle( + inherit: false, + color: iconColor, + fontSize: iconSize, + fontWeight: FontWeight.w900, + fontFamily: icon!.fontFamily, + package: icon!.fontPackage, + shadows: iconShadows, + ), + ), + ); + + if (icon!.matchTextDirection) { + switch (textDirection) { + case TextDirection.rtl: + iconWidget = Transform( + transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0), + alignment: Alignment.center, + transformHitTests: false, + child: iconWidget, + ); + break; + case TextDirection.ltr: + break; + } + } + + return ExcludeSemantics( + child: SizedBox( + width: iconSize, + height: iconSize, + child: Center( + child: iconWidget, + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + IconDataProperty('icon', icon, ifNull: '', showName: false)); + properties.add(DoubleProperty('size', size, defaultValue: null)); + properties.add(ColorProperty('color', color, defaultValue: null)); + } +} diff --git a/lib/src/controls/inputs/toggle_switch.dart b/lib/src/controls/inputs/toggle_switch.dart index 464d3bec7..39a223489 100644 --- a/lib/src/controls/inputs/toggle_switch.dart +++ b/lib/src/controls/inputs/toggle_switch.dart @@ -2,6 +2,11 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +typedef ToggleSwitchThumbBuilder = Widget Function( + BuildContext context, + Set states, +); + /// The toggle switch represents a physical switch that allows users to /// turn things on or off, like a light switch. Use toggle switch controls /// to present users with two mutually exclusive options (such as on/off), @@ -32,8 +37,10 @@ class ToggleSwitch extends StatefulWidget { required this.onChanged, this.style, this.content, + this.leadingContent = false, this.semanticLabel, this.thumb, + this.thumbBuilder, this.focusNode, this.autofocus = false, }) : super(key: key); @@ -50,9 +57,20 @@ class ToggleSwitch extends StatefulWidget { /// The thumb of the switch /// - /// If null, [DefaultToggleSwitchThumb] is used + /// [DefaultToggleSwitchThumb] is used by default + /// + /// See also: + /// * [thumbBuilder], which builds the thumb based on the current state + /// * [DefaultToggleSwitchThumb], used when both [thumb] and [thumbBuilder] are null final Widget? thumb; + /// Build the thumb of the switch based on the current state + /// + /// See also: + /// * [thumb], a static thumb + /// * [DefaultToggleSwitchThumb], used when both [thumb] and [thumbBuilder] are null + final ToggleSwitchThumbBuilder? thumbBuilder; + /// The style of the toggle switch final ToggleSwitchThemeData? style; @@ -64,6 +82,11 @@ class ToggleSwitch extends StatefulWidget { /// Usually a [Text] or [Icon] widget final Widget? content; + /// Whether to position [content] before the switch, if provided + /// + /// Defaults to `false` + final bool leadingContent; + /// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel} final String? semanticLabel; @@ -78,6 +101,8 @@ class ToggleSwitch extends StatefulWidget { super.debugFillProperties(properties); properties ..add(FlagProperty('checked', value: checked, ifFalse: 'unchecked')) + ..add(FlagProperty('leadingContent', + value: leadingContent, ifFalse: 'trailingContent')) ..add(ObjectFlagProperty('onChanged', onChanged, ifNull: 'disabled')) ..add( FlagProperty('autofocus', value: autofocus, ifFalse: 'manual focus')) @@ -155,6 +180,7 @@ class _ToggleSwitchState extends State { ? style.checkedDecoration?.resolve(states) : style.uncheckedDecoration?.resolve(states), child: widget.thumb ?? + widget.thumbBuilder?.call(context, states) ?? DefaultToggleSwitchThumb( checked: widget.checked, style: style, @@ -162,11 +188,20 @@ class _ToggleSwitchState extends State { ), ); if (widget.content != null) { - child = Row(mainAxisSize: MainAxisSize.min, children: [ - child, - const SizedBox(width: 10.0), - widget.content!, - ]); + child = Row( + mainAxisSize: MainAxisSize.min, + children: widget.leadingContent + ? [ + widget.content!, + const SizedBox(width: 10.0), + child, + ] + : [ + child, + const SizedBox(width: 10.0), + widget.content!, + ], + ); } return Semantics( checked: widget.checked, diff --git a/lib/src/controls/surfaces/tooltip.dart b/lib/src/controls/surfaces/tooltip.dart index 4bda4011a..a53728764 100644 --- a/lib/src/controls/surfaces/tooltip.dart +++ b/lib/src/controls/surfaces/tooltip.dart @@ -6,7 +6,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/services.dart'; /// A tooltip is a short description that is linked to another /// control or object. Tooltips help users understand unfamiliar @@ -362,7 +361,7 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { _showTooltip(); } - void _handleMouseExit({bool immediately = true}) { + void _handleMouseExit({bool immediately = false}) { // If the tip is currently covered, we can just remove it without waiting. _dismissTooltip(immediately: _isConcealed || immediately); } @@ -392,8 +391,6 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { height: height, padding: padding, margin: margin, - onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null, - onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null, decoration: decoration, textStyle: textStyle, animation: CurvedAnimation( @@ -459,11 +456,11 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { @override void dispose() { + _removeEntry(); GestureBinding.instance.pointerRouter .removeGlobalRoute(_handlePointerEvent); RendererBinding.instance.mouseTracker .removeListener(_handleMouseTrackerChange); - _removeEntry(); _controller.dispose(); super.dispose(); } @@ -856,8 +853,6 @@ class _TooltipOverlay extends StatelessWidget { required this.verticalOffset, required this.preferBelow, this.displayHorizontally = false, - this.onEnter, - this.onExit, }) : super(key: key); final InlineSpan richMessage; @@ -871,41 +866,33 @@ class _TooltipOverlay extends StatelessWidget { final double verticalOffset; final bool preferBelow; final bool displayHorizontally; - final PointerEnterEventListener? onEnter; - final PointerExitEventListener? onExit; @override Widget build(BuildContext context) { Widget result = IgnorePointer( - child: FadeTransition( - opacity: animation, - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: height), - child: DefaultTextStyle( - style: FluentTheme.of(context).typography.body!, - child: Container( - decoration: decoration, - padding: padding, - margin: margin, - child: Center( - widthFactor: 1.0, - heightFactor: 1.0, - child: Text.rich( - richMessage, - style: textStyle, + child: FadeTransition( + opacity: animation, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: height), + child: DefaultTextStyle( + style: FluentTheme.of(context).typography.body!, + child: Container( + decoration: decoration, + padding: padding, + margin: margin, + child: Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: Text.rich( + richMessage, + style: textStyle, + ), ), ), ), ), ), - )); - if (onEnter != null || onExit != null) { - result = MouseRegion( - onEnter: onEnter, - onExit: onExit, - child: result, - ); - } + ); return Positioned.fill( child: CustomSingleChildLayout( delegate: _TooltipPositionDelegate(