buildFluentDropdown function
- FluentDropdownBaseState state,
- FluentDropdownStyle style,
- Set<
WidgetState> states
Renders a dropdown trigger from a resolved state and style.
The third of the three-function recomposition contract. Takes FluentDropdownBaseState rather than FluentDropdownState on purpose: it never reads the appearance or the size, so a consumer can supply their own style and still use Fluent's layout and accent animation. It renders the trigger only — the popup is an Overlay concern, and its surface has its own builder in buildFluentDropdownSurface.
Motion
One thing animates, and it is the bottom accent rule: upstream's ::after
is transform: scaleX(0) at rest and scaleX(1) under :focus-within, so
the brand rule grows from the centre outwards. See
fluentDropdownAccentEnter and fluentDropdownAccentExit for the two
durations. Nothing else moves — the fill, the border and the chevron all
change on the frame the pointer arrives, because useDropdownStyles
declares no transition on any of them.
states is the live interaction set from FluentInteractive.
Implementation
Widget buildFluentDropdown(
FluentDropdownBaseState state,
FluentDropdownStyle style,
Set<WidgetState> states,
) {
final radius = style.borderRadius?.resolve(states) ?? FluentRadius.allMedium;
final borderWidth = style.borderWidth?.resolve(states) ?? FluentStroke.none;
final borderColor = style.borderColor?.resolve(states);
final underlineColor = style.underlineColor?.resolve(states);
final accentColor = style.accentColor?.resolve(states);
final accentWidth = style.accentWidth?.resolve(states) ?? FluentStroke.thick;
final foreground = state.value == null
? style.placeholderColor?.resolve(states)
: style.foregroundColor?.resolve(states);
final textStyle = style.textStyle?.resolve(states);
final padding = style.padding?.resolve(states) ?? EdgeInsets.zero;
final gap = style.gap?.resolve(states) ?? FluentSpacing.mNudge;
final chevronColor = style.chevronColor?.resolve(states);
final chevronSize = style.chevronSize?.resolve(states) ?? FluentSize.size200;
final chevronPadding =
style.chevronPadding?.resolve(states) ?? EdgeInsets.zero;
final minimumSize = style.minimumSize?.resolve(states) ?? Size.zero;
Widget label = Padding(
padding: padding,
// heightFactor, or the Align fills the whole loose height it is offered and
// a dropdown in a Column becomes as tall as the screen.
child: Align(
alignment: AlignmentDirectional.centerStart,
heightFactor: 1,
child: state.value ?? state.placeholder ?? const SizedBox.shrink(),
),
);
if (textStyle != null || foreground != null) {
label = DefaultTextStyle.merge(
style: (textStyle ?? const TextStyle()).copyWith(color: foreground),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: label,
);
}
final content = Row(
children: <Widget>[
Expanded(child: label),
SizedBox(width: gap),
Padding(
padding: chevronPadding,
child: IconTheme.merge(
data: IconThemeData(color: chevronColor, size: chevronSize),
child: state.chevron,
),
),
],
);
// The rule at rest, and the brand rule that grows over it. Figma draws both
// as rules pinned to the bottom edge — 1px for the resting one, 2px for the
// accent — and BOTH carry the box's bottom radii, so neither runs past the
// curve at the ends.
final rules = <Widget>[
if (underlineColor != null)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SizedBox(
height: FluentStroke.thin,
// Rounded, not a bare rectangle. Upstream draws this as the field's
// own `border-bottom`, so it follows the corner radius for free; we
// draw it as a separate rule because Figma's `Thin underline` is its
// own rect (the Transparent appearance has no border yet still shows
// it) and Flutter asserts on a non-uniform border under a
// borderRadius. FluentInputUnderline is how a 1px rule keeps a 4px
// corner anyway.
child: FluentInputUnderline(
color: underlineColor,
thickness: FluentStroke.thin,
borderRadius: BorderRadius.only(
bottomLeft: radius.bottomLeft,
bottomRight: radius.bottomRight,
),
),
),
),
if (accentColor != null)
Positioned(
left: 0,
right: 0,
bottom: 0,
height: accentWidth,
child: FluentInputFocusUnderline(
focused: state.open || states.contains(WidgetState.focused),
color: accentColor,
thickness: accentWidth,
borderRadius: BorderRadius.only(
bottomLeft: radius.bottomLeft,
bottomRight: radius.bottomRight,
),
),
),
];
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: minimumSize.height,
minWidth: minimumSize.width,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: style.backgroundColor?.resolve(states),
borderRadius: radius,
border: borderWidth > 0 && borderColor != null
? Border.all(color: borderColor, width: borderWidth)
: null,
),
child: Stack(children: <Widget>[content, ...rules]),
),
);
}