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(
// Upstream's action slot is `alignSelf: 'start'`
// (useDialogTitleStyles.styles.raw.js:33-39), so the close sits at the
// top of a title that wraps rather than centred against it.
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,
// Upstream's close is `color: inherit`, so the surface's
// foreground reaches a glyph that names no colour of its own.
child: IconTheme.merge(
data: IconThemeData(color: foreground),
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,
),
),
),
);
}