toApproximateMaterialTheme method

  1. @experimental
ThemeData toApproximateMaterialTheme()

Converts this FThemeData to a Material ThemeData on a best-effort basis.

This method enables interoperability between Forui and Material Design widgets by mapping Forui's theme properties to their closest Material equivalents. Use this when you need to:

  • Use Material widgets within a Forui application
  • Apply your Forui theme consistently to both Forui and Material widgets
  • Create a gradual migration path from Material Design to Forui

Note that this conversion is approximate and experimental. Some styling properties may not map perfectly between the two design systems, and the resulting Material theme might not capture all the nuances of your Forui theme.

// Apply a Forui theme to Material widgets
MaterialApp(
  theme: FThemes.zinc.light.toApproximateMaterialTheme(),
  // ...
)

This method can be generated inside projects and directly modified by running:

dart run forui snippet create material-mapping

Implementation

@experimental
ThemeData toApproximateMaterialTheme() {
  // Material requires height to be 1, certain widgets will overflow without it.
  // TextBaseline.alphabetic is required as TextField requires it.
  final textTheme = TextTheme(
    displayLarge: typography.xl4.copyWith(
      height: 1,
      textBaseline: typography.xl4.textBaseline ?? TextBaseline.alphabetic,
    ),
    displayMedium: typography.xl3.copyWith(
      height: 1,
      textBaseline: typography.xl3.textBaseline ?? TextBaseline.alphabetic,
    ),
    displaySmall: typography.xl2.copyWith(
      height: 1,
      textBaseline: typography.xl2.textBaseline ?? TextBaseline.alphabetic,
    ),
    headlineLarge: typography.xl3.copyWith(
      height: 1,
      textBaseline: typography.xl3.textBaseline ?? TextBaseline.alphabetic,
    ),
    headlineMedium: typography.xl2.copyWith(
      height: 1,
      textBaseline: typography.xl2.textBaseline ?? TextBaseline.alphabetic,
    ),
    headlineSmall: typography.xl.copyWith(
      height: 1,
      textBaseline: typography.xl.textBaseline ?? TextBaseline.alphabetic,
    ),
    titleLarge: typography.lg.copyWith(
      height: 1,
      textBaseline: typography.lg.textBaseline ?? TextBaseline.alphabetic,
    ),
    titleMedium: typography.base.copyWith(
      height: 1,
      textBaseline: typography.base.textBaseline ?? TextBaseline.alphabetic,
    ),
    titleSmall: typography.sm.copyWith(
      height: 1,
      textBaseline: typography.sm.textBaseline ?? TextBaseline.alphabetic,
    ),
    labelLarge: typography.base.copyWith(
      height: 1,
      textBaseline: typography.base.textBaseline ?? TextBaseline.alphabetic,
    ),
    labelMedium: typography.sm.copyWith(
      height: 1,
      textBaseline: typography.sm.textBaseline ?? TextBaseline.alphabetic,
    ),
    labelSmall: typography.xs.copyWith(
      height: 1,
      textBaseline: typography.xs.textBaseline ?? TextBaseline.alphabetic,
    ),
    bodyLarge: typography.base.copyWith(
      height: 1,
      textBaseline: typography.base.textBaseline ?? TextBaseline.alphabetic,
    ),
    bodyMedium: typography.sm.copyWith(
      height: 1,
      textBaseline: typography.sm.textBaseline ?? TextBaseline.alphabetic,
    ),
    bodySmall: typography.xs.copyWith(height: 1, textBaseline: typography.xs.textBaseline ?? TextBaseline.alphabetic),
  )..apply(fontFamily: typography.defaultFontFamily, bodyColor: colors.foreground, displayColor: colors.foreground);

  return ThemeData(
    colorScheme: ColorScheme(
      brightness: colors.brightness,
      primary: colors.primary,
      onPrimary: colors.primaryForeground,
      secondary: colors.secondary,
      onSecondary: colors.secondaryForeground,
      error: colors.error,
      onError: colors.errorForeground,
      surface: colors.background,
      onSurface: colors.foreground,
      secondaryContainer: colors.secondary,
      onSecondaryContainer: colors.secondaryForeground,
    ),
    fontFamily: typography.defaultFontFamily,
    typography: Typography(
      black: textTheme,
      white: textTheme,
      englishLike: textTheme,
      dense: textTheme,
      tall: textTheme,
    ),
    textTheme: textTheme,
    splashFactory: NoSplash.splashFactory,
    useMaterial3: true,

    //// Navigation Bar
    navigationBarTheme: NavigationBarThemeData(
      indicatorShape: RoundedRectangleBorder(borderRadius: style.borderRadius),
    ),

    //// Navigation Drawer
    navigationDrawerTheme: NavigationDrawerThemeData(
      indicatorShape: RoundedRectangleBorder(borderRadius: style.borderRadius),
    ),

    //// Navigation Rail
    navigationRailTheme: NavigationRailThemeData(
      indicatorShape: RoundedRectangleBorder(borderRadius: style.borderRadius),
    ),

    //// Card
    cardTheme: CardThemeData(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: style.borderRadius,
        side: BorderSide(width: style.borderWidth, color: colors.border),
      ),
    ),

    //// Chip
    chipTheme: ChipThemeData(shape: RoundedRectangleBorder(borderRadius: style.borderRadius)),

    //// Input
    inputDecorationTheme: InputDecorationTheme(
      border: WidgetStateInputBorder.resolveWith(textFieldStyle.border.resolve),
      labelStyle: textFieldStyle.descriptionTextStyle.maybeResolve({}),
      floatingLabelStyle: textFieldStyle.labelTextStyle.maybeResolve({}),
      hintStyle: textFieldStyle.hintTextStyle.maybeResolve({}),
      errorStyle: textFieldStyle.errorTextStyle,
      helperStyle: textFieldStyle.descriptionTextStyle.maybeResolve({}),
      counterStyle: textFieldStyle.counterTextStyle.maybeResolve({}),
      contentPadding: textFieldStyle.contentPadding,
    ),

    //// Date Picker
    datePickerTheme: DatePickerThemeData(
      shape: RoundedRectangleBorder(borderRadius: style.borderRadius),
      dayShape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: style.borderRadius)),
      rangePickerShape: RoundedRectangleBorder(borderRadius: style.borderRadius),
    ),

    //// Time Picker
    timePickerTheme: TimePickerThemeData(
      hourMinuteTextColor: colors.secondaryForeground,
      hourMinuteColor: colors.secondary,
      hourMinuteShape: RoundedRectangleBorder(borderRadius: style.borderRadius),
      dayPeriodTextColor: colors.foreground,
      dayPeriodColor: colors.secondary,
      dayPeriodBorderSide: BorderSide(color: colors.border),
      dayPeriodShape: RoundedRectangleBorder(borderRadius: style.borderRadius),
      dialBackgroundColor: colors.secondary,
      shape: RoundedRectangleBorder(borderRadius: style.borderRadius),
    ),

    /// Slider
    sliderTheme: SliderThemeData(
      activeTrackColor: sliderStyles.horizontalStyle.activeColor.maybeResolve({}),
      inactiveTrackColor: sliderStyles.horizontalStyle.inactiveColor.maybeResolve({}),
      disabledActiveTrackColor: sliderStyles.horizontalStyle.activeColor.maybeResolve({WidgetState.disabled}),
      disabledInactiveTrackColor: sliderStyles.horizontalStyle.inactiveColor.maybeResolve({WidgetState.disabled}),
      activeTickMarkColor: sliderStyles.horizontalStyle.markStyle.tickColor.maybeResolve({}),
      inactiveTickMarkColor: sliderStyles.horizontalStyle.markStyle.tickColor.maybeResolve({}),
      disabledActiveTickMarkColor: sliderStyles.horizontalStyle.markStyle.tickColor.maybeResolve({
        WidgetState.disabled,
      }),
      disabledInactiveTickMarkColor: sliderStyles.horizontalStyle.markStyle.tickColor.maybeResolve({
        WidgetState.disabled,
      }),
      thumbColor: sliderStyles.horizontalStyle.thumbStyle.borderColor.maybeResolve({}),
      disabledThumbColor: sliderStyles.horizontalStyle.thumbStyle.borderColor.maybeResolve({WidgetState.disabled}),
      valueIndicatorColor: sliderStyles.horizontalStyle.tooltipStyle.decoration.color,
      valueIndicatorTextStyle: sliderStyles.horizontalStyle.tooltipStyle.textStyle,
    ),

    //// Switch
    switchTheme: SwitchThemeData(
      thumbColor: switchStyle.thumbColor,
      trackColor: switchStyle.trackColor,
      trackOutlineColor: switchStyle.trackColor,
    ),

    //// Buttons
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ButtonStyle(
        textStyle: buttonStyles.secondary.contentStyle.textStyle,
        backgroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.secondary.decoration.maybeResolve(states)?.color ?? colors.secondary,
        ),
        foregroundColor: WidgetStateColor.resolveWith(
          (states) =>
              buttonStyles.secondary.contentStyle.textStyle.maybeResolve(states)?.color ?? colors.secondaryForeground,
        ),
        padding: WidgetStateProperty.all(buttonStyles.secondary.contentStyle.padding),
        shape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: style.borderRadius)),
      ),
    ),
    filledButtonTheme: FilledButtonThemeData(
      style: ButtonStyle(
        textStyle: buttonStyles.primary.contentStyle.textStyle,
        backgroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.primary.decoration.maybeResolve(states)?.color ?? colors.secondary,
        ),
        foregroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.secondary.decoration.maybeResolve(states)?.color ?? colors.secondaryForeground,
        ),
        padding: WidgetStateProperty.all(buttonStyles.primary.contentStyle.padding),
        shape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: style.borderRadius)),
      ),
    ),
    outlinedButtonTheme: OutlinedButtonThemeData(
      style: ButtonStyle(
        textStyle: buttonStyles.outline.contentStyle.textStyle,
        backgroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.outline.decoration.maybeResolve(states)?.color ?? Colors.transparent,
        ),
        foregroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.outline.decoration.maybeResolve(states)?.color ?? Colors.transparent,
        ),
        padding: WidgetStateProperty.all(buttonStyles.outline.contentStyle.padding),
        side: WidgetStateBorderSide.resolveWith((states) {
          final border = buttonStyles.outline.decoration.maybeResolve(states)?.border;
          return BorderSide(
            color:
                border?.top.color ??
                switch (states) {
                  _ when states.contains(WidgetState.disabled) => colors.disable(colors.border),
                  _ when states.contains(WidgetState.hovered) => colors.hover(colors.border),
                  _ => colors.border,
                },
            width: border?.top.width ?? style.borderWidth,
          );
        }),
        shape: WidgetStateProperty.resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius: buttonStyles.outline.decoration.maybeResolve(states)?.borderRadius ?? style.borderRadius,
          ),
        ),
      ),
    ),
    textButtonTheme: TextButtonThemeData(
      style: ButtonStyle(
        textStyle: buttonStyles.ghost.contentStyle.textStyle,
        backgroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.ghost.decoration.maybeResolve(states)?.color ?? Colors.transparent,
        ),
        foregroundColor: WidgetStateColor.resolveWith(
          (states) =>
              buttonStyles.ghost.contentStyle.textStyle.maybeResolve(states)?.color ?? colors.secondaryForeground,
        ),
        shape: WidgetStateProperty.resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius: buttonStyles.ghost.decoration.maybeResolve(states)?.borderRadius ?? style.borderRadius,
          ),
        ),
      ),
    ),
    floatingActionButtonTheme: FloatingActionButtonThemeData(
      backgroundColor: buttonStyles.primary.decoration.maybeResolve(const {})?.color,
      foregroundColor: buttonStyles.primary.contentStyle.textStyle.maybeResolve(const {})?.color,
      hoverColor: buttonStyles.primary.decoration.maybeResolve(const {WidgetState.hovered})?.color,
      disabledElevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: buttonStyles.primary.decoration.maybeResolve(const {})?.borderRadius ?? style.borderRadius,
      ),
    ),
    iconButtonTheme: IconButtonThemeData(
      style: ButtonStyle(
        backgroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.ghost.decoration.maybeResolve(states)?.color ?? Colors.transparent,
        ),
        foregroundColor: WidgetStateColor.resolveWith(
          (states) =>
              buttonStyles.ghost.contentStyle.textStyle.maybeResolve(states)?.color ?? colors.secondaryForeground,
        ),
        shape: WidgetStateProperty.resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius: buttonStyles.ghost.decoration.maybeResolve(states)?.borderRadius ?? style.borderRadius,
          ),
        ),
      ),
    ),
    segmentedButtonTheme: SegmentedButtonThemeData(
      style: ButtonStyle(
        textStyle: buttonStyles.ghost.contentStyle.textStyle,
        backgroundColor: WidgetStateColor.resolveWith(
          (states) => buttonStyles.ghost.decoration.maybeResolve(states)?.color ?? Colors.transparent,
        ),
        foregroundColor: WidgetStateColor.resolveWith(
          (states) =>
              buttonStyles.ghost.contentStyle.textStyle.maybeResolve(states)?.color ?? colors.secondaryForeground,
        ),
        shape: WidgetStateProperty.resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius: buttonStyles.ghost.decoration.maybeResolve(states)?.borderRadius ?? style.borderRadius,
          ),
        ),
      ),
    ),

    /// Dialog
    dialogTheme: DialogThemeData(shape: RoundedRectangleBorder(borderRadius: style.borderRadius)),

    /// Bottom Sheet
    bottomSheetTheme: BottomSheetThemeData(shape: RoundedRectangleBorder(borderRadius: style.borderRadius)),

    /// Snack Bar
    snackBarTheme: SnackBarThemeData(shape: RoundedRectangleBorder(borderRadius: style.borderRadius)),

    /// List Tile
    listTileTheme: ListTileThemeData(shape: RoundedRectangleBorder(borderRadius: style.borderRadius)),

    /// Divider
    dividerTheme: DividerThemeData(
      color: dividerStyles.horizontalStyle.color,
      thickness: dividerStyles.horizontalStyle.width,
    ),

    iconTheme: IconThemeData(color: colors.primary, size: 20),
  );
}