buildFluentDialog function
- FluentDialogBaseState state,
- FluentDialogStyle style,
- Set<
WidgetState> states
Renders a dialog surface from a resolved state and style.
The third of the three-function recomposition contract. Takes FluentDialogBaseState rather than FluentDialogState on purpose: it never reads the size or the modal type, so a consumer can supply their own style and still use Fluent's layout. It renders the surface only — the scrim, the focus trap and the entrance are FluentDialog's concern, because all three exist relative to the Overlay rather than to this box.
The content scrolls, so the surface has to sit in a bounded-height
parent — FluentDialog centres it in the overlay, which is exactly the
maxHeight: 100vh upstream pins on DialogSurface.
Motion
None here. The surface arrives and leaves through fluentDialogSurfaceEnter and fluentDialogSurfaceExit, which are applied by FluentDialog around this widget — nothing inside the surface transitions, because upstream declares no transition on any dialog part.
states is present for symmetry with the rest of the package. A dialog
surface is never hovered, pressed or focused as a whole, so callers normally
pass an empty set; WidgetState.disabled reaches the close button and
nothing else.
Implementation
Widget buildFluentDialog(
FluentDialogBaseState state,
FluentDialogStyle style,
Set<WidgetState> states,
) {
final background = style.backgroundColor?.resolve(states);
final foreground = style.foregroundColor?.resolve(states);
final borderColor = style.borderColor?.resolve(states);
final borderWidth = style.borderWidth?.resolve(states) ?? FluentStroke.none;
final radius = style.borderRadius?.resolve(states) ?? FluentRadius.allXLarge;
final padding = style.padding?.resolve(states) ?? EdgeInsets.zero;
final gap = style.gap?.resolve(states) ?? FluentSpacing.s;
final headerGap = style.headerGap?.resolve(states) ?? FluentSpacing.s;
final closePadding =
style.closeButtonPadding?.resolve(states) ?? EdgeInsets.zero;
final actionsPadding =
style.actionsPadding?.resolve(states) ?? EdgeInsets.zero;
final actionsGap = style.actionsGap?.resolve(states) ?? FluentSpacing.s;
final titleStyle = style.titleTextStyle?.resolve(states);
final bodyStyle = style.bodyTextStyle?.resolve(states);
final maxWidth = style.maxWidth?.resolve(states) ?? double.infinity;
final hasActions =
state.actions.isNotEmpty || state.secondaryActions.isNotEmpty;
final rows = <Widget>[
if (state.title != null || state.closeButton != null)
Row(
// The close button is 32 tall against a 28 title line, and Figma lets
// it overhang rather than centring it. Starting both keeps the glyph
// level with the first line of a title that wraps.
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: DefaultTextStyle.merge(
style: (titleStyle ?? const TextStyle()).copyWith(
color: foreground,
),
child: state.title ?? const SizedBox.shrink(),
),
),
if (state.closeButton != null) ...<Widget>[
SizedBox(width: headerGap),
Padding(padding: closePadding, child: state.closeButton!),
],
],
),
// `useDialogContentStyles` is `overflowY: 'auto'` under a surface capped at
// `maxHeight: ['100vh', '100dvh']`, so a body taller than the viewport
// scrolls while the title and the actions stay put. `Flexible` is that cap:
// the surface is centred in the overlay, which hands it the viewport as its
// maximum height, and the body takes whatever the header and the actions
// leave. Without it a long body is a RenderFlex overflow.
Flexible(
child: SingleChildScrollView(
child: DefaultTextStyle.merge(
style: (bodyStyle ?? const TextStyle()).copyWith(color: foreground),
child: state.content,
),
),
),
if (hasActions)
Padding(padding: actionsPadding, child: _buildActions(state, actionsGap)),
];
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: DecoratedBox(
decoration: BoxDecoration(
color: background,
borderRadius: radius,
border: borderWidth > 0 && borderColor != null
? Border.all(color: borderColor, width: borderWidth)
: null,
boxShadow: style.shadow?.resolve(states),
),
child: Padding(
padding: padding,
// stretch, so the surface takes the full `maxWidth` the way a fixed
// block with `inset: 0; margin: auto` does, rather than hugging its
// widest child.
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: gap,
children: rows,
),
),
),
);
}