buildFluentDialog function

Widget buildFluentDialog(
  1. FluentDialogBaseState state,
  2. FluentDialogStyle style,
  3. 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,
        ),
      ),
    ),
  );
}