toApproximateMaterialTheme method

ThemeData toApproximateMaterialTheme()

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

It does not take into account any platform-specific styling. If you need to do so, consider generating and customizing this method using the CLI:

dart run forui snippet create material-mapping

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. 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.neutral.light.toApproximateMaterialTheme(),
  // ...
)

Implementation

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 ?? .alphabetic),
    displayMedium: typography.xl3.copyWith(height: 1, textBaseline: typography.xl3.textBaseline ?? .alphabetic),
    displaySmall: typography.xl2.copyWith(height: 1, textBaseline: typography.xl2.textBaseline ?? .alphabetic),
    headlineLarge: typography.xl3.copyWith(height: 1, textBaseline: typography.xl3.textBaseline ?? .alphabetic),
    headlineMedium: typography.xl2.copyWith(height: 1, textBaseline: typography.xl2.textBaseline ?? .alphabetic),
    headlineSmall: typography.xl.copyWith(height: 1, textBaseline: typography.xl.textBaseline ?? .alphabetic),
    titleLarge: typography.lg.copyWith(height: 1, textBaseline: typography.lg.textBaseline ?? .alphabetic),
    titleMedium: typography.base.copyWith(height: 1, textBaseline: typography.base.textBaseline ?? .alphabetic),
    titleSmall: typography.sm.copyWith(height: 1, textBaseline: typography.sm.textBaseline ?? .alphabetic),
    labelLarge: typography.base.copyWith(height: 1, textBaseline: typography.base.textBaseline ?? .alphabetic),
    labelMedium: typography.sm.copyWith(height: 1, textBaseline: typography.sm.textBaseline ?? .alphabetic),
    labelSmall: typography.xs.copyWith(height: 1, textBaseline: typography.xs.textBaseline ?? .alphabetic),
    bodyLarge: typography.base.copyWith(height: 1, textBaseline: typography.base.textBaseline ?? .alphabetic),
    bodyMedium: typography.sm.copyWith(height: 1, textBaseline: typography.sm.textBaseline ?? .alphabetic),
    bodySmall: typography.xs.copyWith(height: 1, textBaseline: typography.xs.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((states) => textFieldStyle.border.resolve(toVariants(states))),
      labelStyle: textFieldStyle.descriptionTextStyle.base,
      floatingLabelStyle: textFieldStyle.labelTextStyle.base,
      hintStyle: textFieldStyle.hintTextStyle.base,
      errorStyle: textFieldStyle.errorTextStyle.base,
      helperStyle: textFieldStyle.descriptionTextStyle.base,
      counterStyle: textFieldStyle.counterTextStyle.base,
      contentPadding: textFieldStyle.contentPadding,
    ),

    // Date Picker
    datePickerTheme: DatePickerThemeData(
      shape: RoundedRectangleBorder(borderRadius: style.borderRadius),
      dayShape: .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.base.activeColor.base,
      inactiveTrackColor: sliderStyles.base.inactiveColor.base,
      disabledActiveTrackColor: sliderStyles.base.activeColor.resolve({FSliderVariant.disabled}),
      disabledInactiveTrackColor: sliderStyles.base.inactiveColor.resolve({FSliderVariant.disabled}),
      activeTickMarkColor: sliderStyles.base.markStyle.tickColor.base,
      inactiveTickMarkColor: sliderStyles.base.markStyle.tickColor.base,
      disabledActiveTickMarkColor: sliderStyles.base.markStyle.tickColor.resolve({FSliderVariant.disabled}),
      disabledInactiveTickMarkColor: sliderStyles.base.markStyle.tickColor.resolve({FSliderVariant.disabled}),
      thumbColor: sliderStyles.base.thumbStyle.borderColor.base,
      disabledThumbColor: sliderStyles.base.thumbStyle.borderColor.resolve({FSliderVariant.disabled}),
      valueIndicatorColor: sliderStyles.base.tooltipStyle.decoration.color,
      valueIndicatorTextStyle: sliderStyles.base.tooltipStyle.textStyle,
    ),

    // Switch
    switchTheme: SwitchThemeData(
      thumbColor: .resolveWith((states) => switchStyle.thumbColor.resolve(toVariants(states))),
      trackColor: .resolveWith((states) => switchStyle.trackColor.resolve(toVariants(states))),
      trackOutlineColor: .resolveWith((states) => switchStyle.trackColor.resolve(toVariants(states))),
    ),

    // Buttons
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ButtonStyle(
        textStyle: .resolveWith(
          (states) => buttonStyles
              .resolve({FButtonVariant.secondary})
              .base
              .contentStyle
              .textStyle
              .resolve(toVariants(states)),
        ),
        backgroundColor: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.secondary}).base.decoration.resolve(toVariants(states)).color ??
              colors.secondary,
        ),
        foregroundColor: .resolveWith(
          (states) =>
              buttonStyles
                  .resolve({FButtonVariant.secondary})
                  .base
                  .contentStyle
                  .textStyle
                  .resolve(toVariants(states))
                  .color ??
              colors.secondaryForeground,
        ),
        padding: .all(buttonStyles.resolve({FButtonVariant.secondary}).base.contentStyle.padding),
        shape: .all(RoundedRectangleBorder(borderRadius: style.borderRadius)),
      ),
    ),

    filledButtonTheme: FilledButtonThemeData(
      style: ButtonStyle(
        textStyle: .resolveWith(
          (states) => buttonStyles.base.base.contentStyle.textStyle.resolve(toVariants(states)),
        ),
        backgroundColor: .resolveWith(
          (states) => buttonStyles.base.base.decoration.resolve(toVariants(states)).color ?? colors.secondary,
        ),
        foregroundColor: .resolveWith(
          (states) =>
              buttonStyles
                  .resolve({FButtonVariant.secondary})
                  .base
                  .contentStyle
                  .textStyle
                  .resolve(toVariants(states))
                  .color ??
              colors.secondaryForeground,
        ),
        padding: .all(buttonStyles.base.base.contentStyle.padding),
        shape: .all(RoundedRectangleBorder(borderRadius: style.borderRadius)),
      ),
    ),

    outlinedButtonTheme: OutlinedButtonThemeData(
      style: ButtonStyle(
        textStyle: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.outline}).base.contentStyle.textStyle.resolve(toVariants(states)),
        ),
        backgroundColor: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.outline}).base.decoration.resolve(toVariants(states)).color ??
              Colors.transparent,
        ),
        foregroundColor: .resolveWith(
          (states) =>
              buttonStyles
                  .resolve({FButtonVariant.outline})
                  .base
                  .contentStyle
                  .textStyle
                  .resolve(toVariants(states))
                  .color ??
              Colors.transparent,
        ),
        padding: .all(buttonStyles.resolve({FButtonVariant.outline}).base.contentStyle.padding),
        side: .resolveWith((states) {
          final border = buttonStyles
              .resolve({FButtonVariant.outline})
              .base
              .decoration
              .resolve(toVariants(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: .resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius:
                buttonStyles
                    .resolve({FButtonVariant.outline})
                    .base
                    .decoration
                    .resolve(toVariants(states))
                    .borderRadius ??
                style.borderRadius,
          ),
        ),
      ),
    ),

    textButtonTheme: TextButtonThemeData(
      style: ButtonStyle(
        textStyle: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.ghost}).base.contentStyle.textStyle.resolve(toVariants(states)),
        ),
        backgroundColor: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.ghost}).base.decoration.resolve(toVariants(states)).color ??
              Colors.transparent,
        ),
        foregroundColor: .resolveWith(
          (states) =>
              buttonStyles
                  .resolve({FButtonVariant.ghost})
                  .base
                  .contentStyle
                  .textStyle
                  .resolve(toVariants(states))
                  .color ??
              colors.secondaryForeground,
        ),
        shape: .resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius:
                buttonStyles
                    .resolve({FButtonVariant.ghost})
                    .base
                    .decoration
                    .resolve(toVariants(states))
                    .borderRadius ??
                style.borderRadius,
          ),
        ),
      ),
    ),

    floatingActionButtonTheme: FloatingActionButtonThemeData(
      backgroundColor: buttonStyles.base.base.decoration.base.color,
      foregroundColor: buttonStyles.base.base.contentStyle.textStyle.base.color,
      hoverColor: buttonStyles.base.base.decoration.resolve({FTappableVariant.hovered}).color,
      disabledElevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: buttonStyles.base.base.decoration.base.borderRadius ?? style.borderRadius,
      ),
    ),

    iconButtonTheme: IconButtonThemeData(
      style: ButtonStyle(
        backgroundColor: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.ghost}).base.decoration.resolve(toVariants(states)).color ??
              Colors.transparent,
        ),
        foregroundColor: .resolveWith(
          (states) =>
              buttonStyles
                  .resolve({FButtonVariant.ghost})
                  .base
                  .contentStyle
                  .textStyle
                  .resolve(toVariants(states))
                  .color ??
              colors.secondaryForeground,
        ),
        shape: .resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius:
                buttonStyles
                    .resolve({FButtonVariant.ghost})
                    .base
                    .decoration
                    .resolve(toVariants(states))
                    .borderRadius ??
                style.borderRadius,
          ),
        ),
      ),
    ),

    segmentedButtonTheme: SegmentedButtonThemeData(
      style: ButtonStyle(
        textStyle: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.ghost}).base.contentStyle.textStyle.resolve(toVariants(states)),
        ),
        backgroundColor: .resolveWith(
          (states) =>
              buttonStyles.resolve({FButtonVariant.ghost}).base.decoration.resolve(toVariants(states)).color ??
              Colors.transparent,
        ),
        foregroundColor: .resolveWith(
          (states) =>
              buttonStyles
                  .resolve({FButtonVariant.ghost})
                  .base
                  .contentStyle
                  .textStyle
                  .resolve(toVariants(states))
                  .color ??
              colors.secondaryForeground,
        ),
        shape: .resolveWith(
          (states) => RoundedRectangleBorder(
            borderRadius:
                buttonStyles
                    .resolve({FButtonVariant.ghost})
                    .base
                    .decoration
                    .resolve(toVariants(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.resolve({}).color,
      thickness: dividerStyles.resolve({}).width,
    ),

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