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(
        // 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,
        ),
      ),
    ),
  );
}