Browse Source

Upgrade components, core, effects and resources modules

Components Module
:sparkles: Introduced two new styleable properties to both MFXControl and MFXLabeled, to allow specifying the initial sizes of a component without relying on JavaFX's CSS properties since once they are set, they cannot be overwritten (bug or not it's a shitty behavior)
:sparkles: Introduced a new method to both MFXControl and MFXLabeled to allow implementations to customize the SceneBuilder integration

:bug: MFXElevatedButton: fixed NullPointerException caused by the elevation property as in some occasions both the oldValue and newValue can be null

:sparkles: Introduced FABs and Extended FABs

:recycle: MFXButtonSkin: listener on the graphic property is not needed, let the BoundLabel handle it
:recycle: MFXButtonSkin: do not apply the listener on the content display property if the button is of type MFXFab

:sparkles: Added two new theming APIs to define components' variants

Core Module
:sparkles: SceneBuilderIntegration: added new method for debugging while in SceneBuilder

Effects Module
:recycle: ElevationLevel: hopefully for the last time, improve the shadows transition animation

Resources Module
:sparkles: Added two new properties for MFXFontIcons and MFXIconWrappers
:sparkles: IconsProviders: added a new no-arg method to create a random MFXFontIcon

Assets
:recycle: Buttons: use the new init sizes properties instead of the JavaFX's ones (pref).
:recycle: Buttons: remove the onSurface parameter as it was unused
:sparkles: Added stylesheets for FABs

Signed-off-by: Alessadro Parisi <alessandro.parisi406@gmail.com>
Alessadro Parisi 2 years ago
parent
commit
e29502e688
38 changed files with 1860 additions and 85 deletions
  1. 1 0
      .idea/inspectionProfiles/Project_Default.xml
  2. 154 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXControl.java
  3. 154 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXLabeled.java
  4. 15 12
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXButton.java
  5. 9 6
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXElevatedButton.java
  6. 2 1
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXFilledButton.java
  7. 2 1
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXOutlinedButton.java
  8. 2 1
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXTextButton.java
  9. 2 1
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXTonalFilledButton.java
  10. 107 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXExtendedFab.java
  11. 127 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFab.java
  12. 102 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFabBase.java
  13. 17 21
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXButtonSkin.java
  14. 34 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/base/Variant.java
  15. 136 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/base/WithVariants.java
  16. 50 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/enums/FABVariants.java
  17. 1 1
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/enums/PseudoClasses.java
  18. 4 1
      modules/components/src/main/java/module-info.java
  19. 121 14
      modules/components/src/test/java/app/buttons/ButtonsPlayground.java
  20. 124 0
      modules/components/src/test/java/interactive/TestInitSize.java
  21. 42 0
      modules/components/src/test/java/interactive/TestVariants.java
  22. 20 0
      modules/core/src/main/java/io/github/palexdev/mfxcore/utils/fx/SceneBuilderIntegration.java
  23. 4 4
      modules/effects/src/main/java/io/github/palexdev/mfxeffects/enums/ElevationLevel.java
  24. 120 0
      modules/resources/src/main/java/io/github/palexdev/mfxresources/base/properties/IconProperty.java
  25. 152 0
      modules/resources/src/main/java/io/github/palexdev/mfxresources/base/properties/WrappedIconProperty.java
  26. 37 0
      modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/IconsProviders.java
  27. 3 4
      modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java
  28. 4 1
      modules/resources/src/main/java/module-info.java
  29. 4 2
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_buttons.scss
  30. 2 3
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_buttons_base.scss
  31. 1 2
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_elevated_button.scss
  32. 143 0
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_extended_fab.scss
  33. 155 0
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_fab.scss
  34. 1 2
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_filled_button.scss
  35. 1 2
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_outlined_button.scss
  36. 1 1
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_text_button.scss
  37. 1 2
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_tonal_filled_button.scss
  38. 5 3
      modules/resources/src/test/java/interactive/IconsTests.java

+ 1 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -975,6 +975,7 @@
     <inspection_tool class="DuplicatedBeanNamesInspection" enabled="true" level="WARNING" enabled_by_default="true" />
     <inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
       <Languages>
+        <language minSize="12" name="Style Sheets" />
         <language minSize="125" name="Java" />
       </Languages>
     </inspection_tool>

+ 154 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXControl.java

@@ -19,12 +19,18 @@
 package io.github.palexdev.mfxcomponents.controls.base;
 
 import io.github.palexdev.mfxcore.base.properties.functional.SupplierProperty;
+import io.github.palexdev.mfxcore.base.properties.styleable.StyleableDoubleProperty;
 import io.github.palexdev.mfxcore.behavior.BehaviorBase;
 import io.github.palexdev.mfxcore.behavior.WithBehavior;
 import io.github.palexdev.mfxcore.controls.SkinBase;
+import io.github.palexdev.mfxcore.utils.fx.StyleUtils;
+import javafx.css.CssMetaData;
+import javafx.css.Styleable;
+import javafx.css.StyleablePropertyFactory;
 import javafx.scene.Node;
 import javafx.scene.control.Control;
 
+import java.util.List;
 import java.util.function.Supplier;
 
 /**
@@ -34,6 +40,9 @@ import java.util.function.Supplier;
  * Extends {@link Control} and implements both {@link WithBehavior} and {@link MFXStyleable}.
  * <p></p>
  * Components that primarily deal with text should extend {@link MFXLabeled} instead.
+ * <p></p>
+ * Design guidelines (like MD3), may specify in the components' specs the initial/minimum sizes for each component.
+ * For this specific purpose, there are two properties that can be set in CSS: {@link #initHeightProperty()}, {@link #initWidthProperty()}.
  *
  * @param <B> the behavior type used by the control
  */
@@ -43,6 +52,34 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 	private final SupplierProperty<B> behaviorProvider = new SupplierProperty<>();
 
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * Applies the sizes specified by {@link #initHeightProperty()} and {@link #initWidthProperty()},
+	 * both set in a CSS stylesheet.
+	 * <p>
+	 * By default, this sets the component's pref height and width, can be overridden to change the behavior.
+	 * <p></p>
+	 * By default, the values are set only if the pref height and width are greater than 0, because
+	 * with 'init' sizes we suppose that the components sizes upon creation are still 0
+	 * The 'force' boolean parameter will skip the check and set them anyway.
+	 */
+	protected void applyInitSizes(boolean force) {
+		double ih = getInitHeight();
+		double iw = getInitWidth();
+		if (force || getPrefHeight() <= 0.0) setPrefHeight(ih);
+		if (force || getPrefWidth() <= 0.0) setPrefWidth(iw);
+	}
+
+	/**
+	 * Subclasses can change the actions to perform if the component is being used in SceneBuilder
+	 * by overriding this method. Typically called automatically on components' initialization.
+	 */
+	protected void sceneBuilderIntegration() {
+	}
+
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
@@ -51,6 +88,123 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 		return null;
 	}
 
+	//================================================================================
+	// Styleable Properties
+	//================================================================================
+	private final StyleableDoubleProperty initHeight = new StyleableDoubleProperty(
+			StyleableProperties.INIT_HEIGHT,
+			this,
+			"initHeight",
+			USE_COMPUTED_SIZE
+	) {
+		@Override
+		public void set(double v) {
+			super.set(v);
+			applyInitSizes(false);
+		}
+	};
+
+	private final StyleableDoubleProperty initWidth = new StyleableDoubleProperty(
+			StyleableProperties.INIT_WIDTH,
+			this,
+			"initWidth",
+			USE_COMPUTED_SIZE
+	) {
+		@Override
+		public void set(double v) {
+			super.set(v);
+			applyInitSizes(false);
+		}
+	};
+
+	protected final double getInitHeight() {
+		return initHeight.get();
+	}
+
+	/**
+	 * Specifies the component's initial height when created.
+	 * <p></p>
+	 * This can be useful when using components that define certain sizes by specs, in
+	 * SceneBuilder and other similar cases. One could also use the '-fx-pref-height' CSS
+	 * property JavaFX offers, but the issue is that once it is set by CSS it won't be possible to
+	 * overwrite the value in some cases. To overcome this, the size can be set via code, this property
+	 * just offers a way to specify the height in CSS and still apply it via code.
+	 * <p>
+	 * The way initial sizes are applied is managed by {@link #applyInitSizes(boolean)}.
+	 * <p></p>
+	 * Can be set in CSS via the property: '-mfx-init-height'.
+	 */
+	protected final StyleableDoubleProperty initHeightProperty() {
+		return initHeight;
+	}
+
+	protected final void setInitHeight(double initHeight) {
+		this.initHeight.set(initHeight);
+	}
+
+	protected final double getInitWidth() {
+		return initWidth.get();
+	}
+
+	/**
+	 * Specifies the component's initial width when created.
+	 * <p></p>
+	 * This can be useful when using components that define certain sizes by specs, in
+	 * SceneBuilder and other similar cases. One could also use the '-fx-pref-width' CSS
+	 * property JavaFX offers, but the issue is that once it is set by CSS it won't be possible to
+	 * overwrite the value in some cases. To overcome this, the size can be set via code, this property
+	 * just offers a way to specify the width in CSS and still apply it via code.
+	 * <p>
+	 * The way initial sizes are applied is managed by {@link #applyInitSizes(boolean)}.
+	 * <p></p>
+	 * Can be set in CSS via the property: '-mfx-init-width'.
+	 */
+	protected final StyleableDoubleProperty initWidthProperty() {
+		return initWidth;
+	}
+
+	protected final void setInitWidth(double initWidth) {
+		this.initWidth.set(initWidth);
+	}
+
+	//================================================================================
+	// CssMetaData
+	//================================================================================
+	private static class StyleableProperties {
+		private static final StyleablePropertyFactory<MFXControl<?>> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
+		private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+		protected static final CssMetaData<MFXControl<?>, Number> INIT_HEIGHT =
+				FACTORY.createSizeCssMetaData(
+						"-mfx-init-height",
+						MFXControl::initHeightProperty,
+						USE_COMPUTED_SIZE
+				);
+
+		private static final CssMetaData<MFXControl<?>, Number> INIT_WIDTH =
+				FACTORY.createSizeCssMetaData(
+						"-mfx-init-width",
+						MFXControl::initWidthProperty,
+						USE_COMPUTED_SIZE
+				);
+
+		static {
+			cssMetaDataList = StyleUtils.cssMetaDataList(
+					Control.getClassCssMetaData(),
+					INIT_HEIGHT, INIT_WIDTH
+			);
+		}
+	}
+
+	public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+		return StyleableProperties.cssMetaDataList;
+	}
+
+	@Override
+	public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+		return getClassCssMetaData();
+	}
+
 	//================================================================================
 	// Getters/Setters
 	//================================================================================

+ 154 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXLabeled.java

@@ -19,12 +19,18 @@
 package io.github.palexdev.mfxcomponents.controls.base;
 
 import io.github.palexdev.mfxcore.base.properties.functional.SupplierProperty;
+import io.github.palexdev.mfxcore.base.properties.styleable.StyleableDoubleProperty;
 import io.github.palexdev.mfxcore.behavior.BehaviorBase;
 import io.github.palexdev.mfxcore.behavior.WithBehavior;
 import io.github.palexdev.mfxcore.controls.SkinBase;
+import io.github.palexdev.mfxcore.utils.fx.StyleUtils;
+import javafx.css.CssMetaData;
+import javafx.css.Styleable;
+import javafx.css.StyleablePropertyFactory;
 import javafx.scene.Node;
 import javafx.scene.control.Labeled;
 
+import java.util.List;
 import java.util.function.Supplier;
 
 /**
@@ -32,6 +38,9 @@ import java.util.function.Supplier;
  * * that perfectly integrates with the new Behavior and Theming APIs.
  * <p>
  * Extends {@link Labeled} and implements both {@link WithBehavior} and {@link MFXStyleable}.
+ * <p></p>
+ * Design guidelines (like MD3), may specify in the components' specs the initial/minimum sizes for each component.
+ * For this specific purpose, there are two properties that can be set in CSS: {@link #initHeightProperty()}, {@link #initWidthProperty()}.
  *
  * @param <B> the behavior type used by the control
  */
@@ -55,6 +64,34 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		super(text, graphic);
 	}
 
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * Applies the sizes specified by {@link #initHeightProperty()} and {@link #initWidthProperty()},
+	 * both set in a CSS stylesheet.
+	 * <p>
+	 * By default, this sets the component's pref height and width, can be overridden to change the behavior.
+	 * <p></p>
+	 * By default, the values are set only if the pref height and width are greater than 0, because
+	 * with 'init' sizes we suppose that the components sizes upon creation are still 0
+	 * The 'force' boolean parameter will skip the check and set them anyway.
+	 */
+	protected void applyInitSizes(boolean force) {
+		double ih = getInitHeight();
+		double iw = getInitWidth();
+		if (force || getPrefHeight() <= 0.0) setPrefHeight(ih);
+		if (force || getPrefWidth() <= 0.0) setPrefWidth(iw);
+	}
+
+	/**
+	 * Subclasses can change the actions to perform if the component is being used in SceneBuilder
+	 * by overriding this method. Typically called automatically on components' initialization.
+	 */
+	protected void sceneBuilderIntegration() {
+	}
+
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
@@ -63,6 +100,123 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		return null;
 	}
 
+	//================================================================================
+	// Styleable Properties
+	//================================================================================
+	private final StyleableDoubleProperty initHeight = new StyleableDoubleProperty(
+			StyleableProperties.INIT_HEIGHT,
+			this,
+			"initHeight",
+			USE_COMPUTED_SIZE
+	) {
+		@Override
+		public void set(double v) {
+			super.set(v);
+			applyInitSizes(false);
+		}
+	};
+
+	private final StyleableDoubleProperty initWidth = new StyleableDoubleProperty(
+			StyleableProperties.INIT_WIDTH,
+			this,
+			"initWidth",
+			USE_COMPUTED_SIZE
+	) {
+		@Override
+		public void set(double v) {
+			super.set(v);
+			applyInitSizes(false);
+		}
+	};
+
+	protected final double getInitHeight() {
+		return initHeight.get();
+	}
+
+	/**
+	 * Specifies the component's initial height when created.
+	 * <p></p>
+	 * This can be useful when using components that define certain sizes by specs, in
+	 * SceneBuilder and other similar cases. One could also use the '-fx-pref-height' CSS
+	 * property JavaFX offers, but the issue is that once it is set by CSS it won't be possible to
+	 * overwrite the value in some cases. To overcome this, the size can be set via code, this property
+	 * just offers a way to specify the height in CSS and still apply it via code.
+	 * <p>
+	 * The way initial sizes are applied is managed by {@link #applyInitSizes(boolean)}.
+	 * <p></p>
+	 * Can be set in CSS via the property: '-mfx-init-height'.
+	 */
+	protected final StyleableDoubleProperty initHeightProperty() {
+		return initHeight;
+	}
+
+	protected final void setInitHeight(double initHeight) {
+		this.initHeight.set(initHeight);
+	}
+
+	protected final double getInitWidth() {
+		return initWidth.get();
+	}
+
+	/**
+	 * Specifies the component's initial width when created.
+	 * <p></p>
+	 * This can be useful when using components that define certain sizes by specs, in
+	 * SceneBuilder and other similar cases. One could also use the '-fx-pref-width' CSS
+	 * property JavaFX offers, but the issue is that once it is set by CSS it won't be possible to
+	 * overwrite the value in some cases. To overcome this, the size can be set via code, this property
+	 * just offers a way to specify the width in CSS and still apply it via code.
+	 * <p>
+	 * The way initial sizes are applied is managed by {@link #applyInitSizes(boolean)}.
+	 * <p></p>
+	 * Can be set in CSS via the property: '-mfx-init-width'.
+	 */
+	protected final StyleableDoubleProperty initWidthProperty() {
+		return initWidth;
+	}
+
+	protected final void setInitWidth(double initWidth) {
+		this.initWidth.set(initWidth);
+	}
+
+	//================================================================================
+	// CssMetaData
+	//================================================================================
+	private static class StyleableProperties {
+		private static final StyleablePropertyFactory<MFXLabeled<?>> FACTORY = new StyleablePropertyFactory<>(Labeled.getClassCssMetaData());
+		private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+		protected static final CssMetaData<MFXLabeled<?>, Number> INIT_HEIGHT =
+				FACTORY.createSizeCssMetaData(
+						"-mfx-init-height",
+						MFXLabeled::initHeightProperty,
+						USE_COMPUTED_SIZE
+				);
+
+		private static final CssMetaData<MFXLabeled<?>, Number> INIT_WIDTH =
+				FACTORY.createSizeCssMetaData(
+						"-mfx-init-width",
+						MFXLabeled::initWidthProperty,
+						USE_COMPUTED_SIZE
+				);
+
+		static {
+			cssMetaDataList = StyleUtils.cssMetaDataList(
+					Labeled.getClassCssMetaData(),
+					INIT_HEIGHT, INIT_WIDTH
+			);
+		}
+	}
+
+	public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+		return StyleableProperties.cssMetaDataList;
+	}
+
+	@Override
+	public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+		return getClassCssMetaData();
+	}
+
 	//================================================================================
 	// Getters/Setters
 	//================================================================================

+ 15 - 12
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXButton.java

@@ -122,18 +122,7 @@ public class MFXButton extends MFXLabeled<MFXButtonBehavior> {
 	private void initialize() {
 		getStyleClass().setAll(defaultStyleClasses());
 		setDefaultBehaviorProvider();
-
-		// SceneBuilder integration
-		SceneBuilderIntegration.ifInSceneBuilder(() -> setText("Button"));
-		SceneBuilderIntegration.ifInSceneBuilder(() -> {
-			String theme = MFXResources.load("sass/md3/mfx-light.css");
-			When.onChanged(sceneProperty())
-					.condition((o, n) -> n != null && !n.getStylesheets().contains(theme))
-					.then((o, n) -> n.getStylesheets().add(theme))
-					.oneShot()
-					.listen();
-		});
-		// TODO theme integration with SceneBuilder will change once base themes and MFXThemeManager are implemented
+		sceneBuilderIntegration();
 	}
 
 	/**
@@ -162,6 +151,20 @@ public class MFXButton extends MFXLabeled<MFXButtonBehavior> {
 		return new MFXButtonSkin(this);
 	}
 
+	@Override
+	protected void sceneBuilderIntegration() {
+		SceneBuilderIntegration.ifInSceneBuilder(() -> setText("Button"));
+		SceneBuilderIntegration.ifInSceneBuilder(() -> {
+			String theme = MFXResources.load("sass/md3/mfx-light.css");
+			When.onChanged(sceneProperty())
+					.condition((o, n) -> n != null && !n.getStylesheets().contains(theme))
+					.then((o, n) -> n.getStylesheets().add(theme))
+					.oneShot()
+					.listen();
+		});
+		// TODO theme integration with SceneBuilder will change once base themes and MFXThemeManager are implemented
+	}
+
 	//================================================================================
 	// Getters/Setters
 	//================================================================================

+ 9 - 6
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXElevatedButton.java

@@ -42,10 +42,11 @@ public class MFXElevatedButton extends MFXButton {
 	// Constructors
 	//================================================================================
 	public MFXElevatedButton() {
+		this("");
 	}
 
 	public MFXElevatedButton(String text) {
-		super(text, null);
+		this(text, null);
 	}
 
 	public MFXElevatedButton(String text, Node icon) {
@@ -74,7 +75,8 @@ public class MFXElevatedButton extends MFXButton {
 	private final StyleableObjectProperty<ElevationLevel> elevation = new StyleableObjectProperty<>(
 			StyleableProperties.ELEVATION,
 			this,
-			"elevation"
+			"elevation",
+			ElevationLevel.LEVEL0
 	) {
 		@Override
 		public void set(ElevationLevel newValue) {
@@ -95,7 +97,7 @@ public class MFXElevatedButton extends MFXButton {
 			}
 
 			ElevationLevel oldValue = get();
-			if (oldValue != newValue)
+			if (oldValue != null && newValue != null && oldValue != newValue)
 				oldValue.animateTo((DropShadow) effect, newValue);
 			super.set(newValue);
 		}
@@ -123,14 +125,15 @@ public class MFXElevatedButton extends MFXButton {
 	// CssMetaData
 	//================================================================================
 	private static class StyleableProperties {
-		private static final StyleablePropertyFactory<io.github.palexdev.mfxcomponents.controls.buttons.MFXElevatedButton> FACTORY = new StyleablePropertyFactory<>(MFXButton.getClassCssMetaData());
+		private static final StyleablePropertyFactory<MFXElevatedButton> FACTORY = new StyleablePropertyFactory<>(MFXButton.getClassCssMetaData());
 		private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
 
-		private static final CssMetaData<io.github.palexdev.mfxcomponents.controls.buttons.MFXElevatedButton, ElevationLevel> ELEVATION =
+		private static final CssMetaData<MFXElevatedButton, ElevationLevel> ELEVATION =
 				FACTORY.createEnumCssMetaData(
 						ElevationLevel.class,
 						"-mfx-elevation",
-						io.github.palexdev.mfxcomponents.controls.buttons.MFXElevatedButton::elevationProperty
+						MFXElevatedButton::elevationProperty,
+						ElevationLevel.LEVEL0
 				);
 
 		static {

+ 2 - 1
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXFilledButton.java

@@ -33,10 +33,11 @@ public class MFXFilledButton extends MFXButton {
 	// Constructors
 	//================================================================================
 	public MFXFilledButton() {
+		this("");
 	}
 
 	public MFXFilledButton(String text) {
-		super(text);
+		this(text, null);
 	}
 
 	public MFXFilledButton(String text, Node icon) {

+ 2 - 1
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXOutlinedButton.java

@@ -33,10 +33,11 @@ public class MFXOutlinedButton extends MFXButton {
 	// Constructors
 	//================================================================================
 	public MFXOutlinedButton() {
+		this("");
 	}
 
 	public MFXOutlinedButton(String text) {
-		super(text);
+		this(text, null);
 	}
 
 	public MFXOutlinedButton(String text, Node icon) {

+ 2 - 1
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXTextButton.java

@@ -33,10 +33,11 @@ public class MFXTextButton extends MFXButton {
 	// Constructors
 	//================================================================================
 	public MFXTextButton() {
+		this("");
 	}
 
 	public MFXTextButton(String text) {
-		super(text);
+		this(text, null);
 	}
 
 	public MFXTextButton(String text, Node icon) {

+ 2 - 1
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXTonalFilledButton.java

@@ -34,10 +34,11 @@ public class MFXTonalFilledButton extends MFXButton {
 	//================================================================================
 
 	public MFXTonalFilledButton() {
+		this("");
 	}
 
 	public MFXTonalFilledButton(String text) {
-		super(text);
+		this(text, null);
 	}
 
 	public MFXTonalFilledButton(String text, Node icon) {

+ 107 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXExtendedFab.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package io.github.palexdev.mfxcomponents.controls.fab;
+
+import io.github.palexdev.mfxcomponents.theming.base.WithVariants;
+import io.github.palexdev.mfxcomponents.theming.enums.FABVariants;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+
+import java.util.List;
+
+/**
+ * Extension of {@link MFXFabBase}. This variant allows to show both the text and icon.
+ * <p></p>
+ * Implements the {@link WithVariants} API, since these type of FABs have slightly different versions, the
+ * variants are described by {@link FABVariants}. However, note that extended FABs do not have small and large
+ * variants, applying those will result in an un-styled component.
+ */
+public class MFXExtendedFab extends MFXFabBase implements WithVariants<MFXExtendedFab, FABVariants> {
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXExtendedFab() {
+		this("");
+	}
+
+	public MFXExtendedFab(String text) {
+		this(text, null);
+	}
+
+	public MFXExtendedFab(MFXFontIcon icon) {
+		this("", icon);
+	}
+
+	public MFXExtendedFab(String text, MFXFontIcon icon) {
+		super(text, icon);
+	}
+
+	/**
+	 * Creates a new {@code MFXExtendedFab}, surface color scheme variant.
+	 */
+	public static MFXExtendedFab surface() {
+		return new MFXExtendedFab().setVariants(FABVariants.SURFACE);
+	}
+
+	/**
+	 * Creates a new {@code MFXExtendedFab}, secondary color scheme variant.
+	 */
+	public static MFXExtendedFab secondary() {
+		return new MFXExtendedFab().setVariants(FABVariants.SECONDARY);
+	}
+
+	/**
+	 * Creates a new {@code MFXExtendedFab}, tertiary color scheme variant.
+	 */
+	public static MFXExtendedFab tertiary() {
+		return new MFXExtendedFab().setVariants(FABVariants.TERTIARY);
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+	@Override
+	public List<String> defaultStyleClasses() {
+		return List.of("mfx-button", "extended-fab");
+	}
+
+	@Override
+	protected void applyInitSizes(boolean force) {
+		double ih = getInitHeight();
+		double iw = getInitWidth();
+		if (force || getPrefHeight() <= 0.0) setPrefHeight(ih);
+		if (force || getMinWidth() <= 0.0) setMinWidth(iw);
+	}
+
+	@Override
+	public MFXExtendedFab addVariants(FABVariants... variants) {
+		WithVariants.addVariants(this, variants);
+		applyInitSizes(true);
+		return this;
+	}
+
+	@Override
+	public MFXExtendedFab setVariants(FABVariants... variants) {
+		WithVariants.setVariants(this, variants);
+		applyInitSizes(true);
+		return this;
+	}
+}
+

+ 127 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFab.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxcomponents.controls.fab;
+
+import io.github.palexdev.mfxcomponents.theming.base.WithVariants;
+import io.github.palexdev.mfxcomponents.theming.enums.FABVariants;
+import io.github.palexdev.mfxcore.observables.When;
+import io.github.palexdev.mfxcore.utils.fx.SceneBuilderIntegration;
+import io.github.palexdev.mfxresources.MFXResources;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+
+import java.util.List;
+
+/**
+ * Extension of {@link MFXFabBase}. This variant only allows icons to be showed, text will always be
+ * set to empty.
+ * <p></p>
+ * Implements the {@link WithVariants} API, since these type of FABs have slightly different versions, the
+ * variants are described by {@link FABVariants}.
+ */
+public class MFXFab extends MFXFabBase implements WithVariants<MFXFab, FABVariants> {
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXFab() {
+		this(null);
+	}
+
+	public MFXFab(MFXFontIcon icon) {
+		super("", icon);
+		initialize();
+	}
+
+	/**
+	 * Creates a new {@code MFXFab}, small variant.
+	 */
+	public static MFXFab small() {
+		return new MFXFab().setVariants(FABVariants.SMALL);
+	}
+
+	/**
+	 * Creates a new {@code MFXFab}, large variant.
+	 */
+	public static MFXFab large() {
+		return new MFXFab().setVariants(FABVariants.LARGE);
+	}
+
+	/**
+	 * Creates a new {@code MFXFab}, surface color scheme variant.
+	 */
+	public static MFXFab surface() {
+		return new MFXFab().setVariants(FABVariants.SURFACE);
+	}
+
+	/**
+	 * Creates a new {@code MFXFab}, secondary color scheme variant.
+	 */
+	public static MFXFab secondary() {
+		return new MFXFab().setVariants(FABVariants.SECONDARY);
+	}
+
+	/**
+	 * Creates a new {@code MFXFab}, tertiary color scheme variant.
+	 */
+	public static MFXFab tertiary() {
+		return new MFXFab().setVariants(FABVariants.TERTIARY);
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+	private void initialize() {
+		textProperty().addListener(i -> setText(""));
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+	@Override
+	public List<String> defaultStyleClasses() {
+		return List.of("mfx-button", "fab");
+	}
+
+	@Override
+	public MFXFab addVariants(FABVariants... variants) {
+		WithVariants.addVariants(this, variants);
+		applyInitSizes(true);
+		return this;
+	}
+
+	@Override
+	public MFXFab setVariants(FABVariants... variants) {
+		WithVariants.setVariants(this, variants);
+		applyInitSizes(true);
+		return this;
+	}
+
+	@Override
+	protected void sceneBuilderIntegration() {
+		SceneBuilderIntegration.ifInSceneBuilder(() -> {
+			String theme = MFXResources.load("sass/md3/mfx-light.css");
+			When.onChanged(sceneProperty())
+					.condition((o, n) -> n != null && !n.getStylesheets().contains(theme))
+					.then((o, n) -> n.getStylesheets().add(theme))
+					.oneShot()
+					.listen();
+		});
+		// TODO theme integration with SceneBuilder will change once base themes and MFXThemeManager are implemented
+	}
+}

+ 102 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFabBase.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxcomponents.controls.fab;
+
+import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton;
+import io.github.palexdev.mfxcomponents.controls.buttons.MFXElevatedButton;
+import io.github.palexdev.mfxresources.base.properties.IconProperty;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+
+import java.util.List;
+
+/**
+ * Extension of {@link MFXElevatedButton} and base class to implement the Floating Action Buttons shown
+ * in the MD3 guidelines.
+ * <p></p>
+ * This base class has two variants: {@link MFXFab} and {@link MFXExtendedFab}.
+ * <p>
+ * This is meant to be used by users that want an untouched base FAB, this component just like {@link MFXButton} is
+ * not styled by the themes by default.
+ * <p>
+ * It's selector in CSS is: '.mfx-button.fab-base'.
+ * <p></p>
+ * Since FABs are meant to be used with icons, these enforce the usage of {@link MFXFontIcon}s
+ * rather than generic nodes.
+ */
+public class MFXFabBase extends MFXElevatedButton {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private final IconProperty icon = new IconProperty();
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXFabBase() {
+		this("");
+	}
+
+	public MFXFabBase(String text) {
+		this(text, null);
+	}
+
+	public MFXFabBase(MFXFontIcon icon) {
+		this("", icon);
+	}
+
+	public MFXFabBase(String text, MFXFontIcon icon) {
+		super(text, icon);
+		initialize();
+		setIcon(icon);
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+	private void initialize() {
+		graphicProperty().bind(icon);
+		sceneBuilderIntegration();
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+	@Override
+	public List<String> defaultStyleClasses() {
+		return List.of("mfx-button", "fab-base");
+	}
+
+	//================================================================================
+	// Getters/Setters
+	//================================================================================
+	public MFXFontIcon getIcon() {
+		return iconProperty().get();
+	}
+
+	/**
+	 * Specifies the FAB's icon.
+	 */
+	public IconProperty iconProperty() {
+		return icon;
+	}
+
+	public void setIcon(MFXFontIcon icon) {
+		iconProperty().set(icon);
+	}
+}

+ 17 - 21
modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXButtonSkin.java

@@ -20,7 +20,8 @@ package io.github.palexdev.mfxcomponents.skins;
 
 import io.github.palexdev.mfxcomponents.behaviors.MFXButtonBehavior;
 import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton;
-import io.github.palexdev.mfxcomponents.theming.PseudoClasses;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFab;
+import io.github.palexdev.mfxcomponents.theming.enums.PseudoClasses;
 import io.github.palexdev.mfxcore.controls.BoundLabel;
 import io.github.palexdev.mfxcore.controls.SkinBase;
 import io.github.palexdev.mfxcore.utils.fx.LayoutUtils;
@@ -76,7 +77,6 @@ public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
 	/**
 	 * Adds the following listeners:
 	 * <p> - A listener on the {@link MFXButton#behaviorProviderProperty()} to update the control's behavior when the provider changes
-	 * <p> - A listener on the {@link MFXButton#graphicProperty()} to update the graphic node when it changes
 	 * <p> - A listener on the {@link MFXButton#contentDisplayProperty()} to activate/disable the pseudo classes
 	 * {@link PseudoClasses#WITH_ICON_LEFT} and {@link PseudoClasses#WITH_ICON_RIGHT} accordingly
 	 */
@@ -86,24 +86,20 @@ public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
 				.then((o, n) -> setBehavior(n.get()))
 				.executeNow()
 				.listen();
-		onChanged(button.graphicProperty())
-				.then((o, n) -> {
-					if (o != null) getChildren().remove(o);
-					if (n != null) getChildren().add(n);
-				})
-				.executeNow()
-				.listen();
-		onChanged(button.contentDisplayProperty())
-				.then((o, n) -> {
-					Node graphic = button.getGraphic();
-					boolean wil = (graphic != null) && (n == ContentDisplay.LEFT);
-					boolean wir = (graphic != null) && (n == ContentDisplay.RIGHT);
-					PseudoClasses.WITH_ICON_LEFT.setOn(button, wil);
-					PseudoClasses.WITH_ICON_RIGHT.setOn(button, wir);
-				})
-				.executeNow()
-				.invalidating(button.graphicProperty())
-				.listen();
+
+		if (!(button instanceof MFXFab)) {
+			onChanged(button.contentDisplayProperty())
+					.then((o, n) -> {
+						Node graphic = button.getGraphic();
+						boolean wil = (graphic != null) && (n == ContentDisplay.LEFT);
+						boolean wir = (graphic != null) && (n == ContentDisplay.RIGHT);
+						PseudoClasses.WITH_ICON_LEFT.setOn(button, wil);
+						PseudoClasses.WITH_ICON_RIGHT.setOn(button, wir);
+					})
+					.executeNow()
+					.invalidating(button.graphicProperty())
+					.listen();
+		}
 	}
 
 	//================================================================================
@@ -162,7 +158,7 @@ public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
 	@Override
 	public void dispose() {
 		MFXButton button = getSkinnable();
-		disposeFor(button.graphicProperty());
+		disposeFor(button.behaviorProviderProperty());
 		disposeFor(button.contentDisplayProperty());
 		super.dispose();
 	}

+ 34 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/base/Variant.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxcomponents.theming.base;
+
+import io.github.palexdev.mfxcomponents.theming.enums.FABVariants;
+
+/**
+ * Depending on the components and the design guidelines, sometimes certain UI elements can have variants
+ * that mainly change in style.
+ * <p>
+ * This API allows in such cases to define variants by specifying a style class that can be applied to a component.
+ * <p></p>
+ * This is typically implemented by Enumerators, see {@link FABVariants} as an example. The public API allows other APIs
+ * (such as {@link WithVariants}) to apply the variant style class to the component.
+ */
+public interface Variant {
+	String variantStyleClass();
+}

+ 136 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/base/WithVariants.java

@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxcomponents.theming.base;
+
+import io.github.palexdev.mfxcomponents.controls.base.MFXControl;
+import io.github.palexdev.mfxcomponents.controls.base.MFXLabeled;
+import io.github.palexdev.mfxcomponents.controls.base.MFXStyleable;
+import javafx.scene.Node;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Simple API for components that have variants of themselves.
+ * <p></p>
+ * A clarification must be made here. Components typically have a number of default style classes, for example
+ * if they implement {@link MFXStyleable}, and additionally they can have variants that change their properties/features/appearance.
+ * This API can be used when creating separate classes is not feasible or just useless.
+ * <p>
+ * To make this more comprehensible consider this example:
+ * <p>
+ * Material Design 3 buttons have five variants: elevated, filled, outlined, text, and tonal filled. Apart from the
+ * elevated variant that includes the 'shadow elevation mechanism', the other ones do not add/change any feature/behavior.
+ * So technically they are good candidates for this API, however they are <b>different</b> types of buttons, each with its
+ * usage/use cases.
+ * <p>
+ * Now on the other hand, consider FABs. Floating Action Buttons mainly have two variants: small and large.
+ * They are the same exact component, what changes is just their sizes, in other words it's not worth defining two
+ * new classes to represent these variants, it's enough to just define the variants by changing the 'base' style classes.
+ * <p></p>
+ * Another way to see is as follows:
+ * <p> MFXElevatedButton has '.mfx-button.elevated' as selector, MFXFilledButton has '.mfx-button.filled' (same is true
+ * for other buttons), notice how the class defining the button style ('.elevated' and '.filled') are different,
+ * the difference between the two buttons is more explicit.
+ * <p>
+ * MFXFab with standard size has '.mfx-button.fab' as selector, and in its small variant has '.mfx-button.fab.small'
+ * as selector, notice how the difference between the two is way less emphasized, they are the same component even in style,
+ * just different size.
+ * <p></p>
+ * The cons in using this mechanism is a less 'comfortable' integration with SceneBuilder. Having separate classes
+ * allows SceneBuilder to detect the variant and add it to the Custom Controls section. This way variants can still be used
+ * by adding the variant style class in the properties inspector, but the user is required to remember/check which
+ * are the applicable classes.
+ * <p></p>
+ * Since components may have multiple variants that can even be combined, this API offers two different ways to apply the
+ * variants:
+ * <p> - A 'set' method, which is intended to be implemented so that the component style classes are first reset to its base ones
+ * and only then the variants are added
+ * <p> - An 'add' method, which is intended to be implemented so that the variants are added to the style classes already
+ * set on the component. A recommendation, is to filter the style classes in a {@code LinkedHashSet} to avoid duplicates
+ * and unwanted behaviors.
+ * <p></p>
+ * Uses the {@link Variant} API.
+ */
+public interface WithVariants<N extends Node, V extends Variant> {
+
+	N addVariants(V... variants);
+
+	N setVariants(V... variants);
+
+	/**
+	 * Adds all the given variants to the given control.
+	 * <p>
+	 * Style classes are filtered by a {@link LinkedHashSet} to avoid duplicates while keeping the specified order.
+	 */
+	@SafeVarargs
+	static <C extends MFXControl<?>, V extends Variant> C addVariants(C control, V... variants) {
+		Set<String> classes = new LinkedHashSet<>(control.getStyleClass());
+		for (V variant : variants) {
+			classes.add(variant.variantStyleClass());
+		}
+		control.getStyleClass().setAll(classes);
+		return control;
+	}
+
+	/**
+	 * Replaces the given control' style classes with its base ones, then adds the specified variants.
+	 * <p>
+	 * Style classes are filtered by a {@link LinkedHashSet} to avoid duplicates while keeping the specified order.
+	 */
+	@SafeVarargs
+	static <C extends MFXControl<?>, V extends Variant> C setVariants(C control, V... variants) {
+		Set<String> classes = new LinkedHashSet<>(control.defaultStyleClasses());
+		for (V variant : variants) {
+			classes.add(variant.variantStyleClass());
+		}
+		control.getStyleClass().setAll(classes);
+		return control;
+	}
+
+	/**
+	 * Adds all the given variants to the given labeled.
+	 * <p>
+	 * Style classes are filtered by a {@link LinkedHashSet} to avoid duplicates while keeping the specified order.
+	 */
+	@SafeVarargs
+	static <L extends MFXLabeled<?>, V extends Variant> L addVariants(L labeled, V... variants) {
+		Set<String> classes = new LinkedHashSet<>(labeled.getStyleClass());
+		for (V variant : variants) {
+			classes.add(variant.variantStyleClass());
+		}
+		labeled.getStyleClass().setAll(classes);
+		return labeled;
+	}
+
+	/**
+	 * Replaces the given labeled' style classes with its base ones, then adds the specified variants.
+	 * <p>
+	 * Style classes are filtered by a {@link LinkedHashSet} to avoid duplicates while keeping the specified order.
+	 */
+	@SafeVarargs
+	static <L extends MFXLabeled<?>, V extends Variant> L setVariants(L labeled, V... variants) {
+		Set<String> classes = new LinkedHashSet<>(labeled.defaultStyleClasses());
+		for (V variant : variants) {
+			classes.add(variant.variantStyleClass());
+		}
+		labeled.getStyleClass().setAll(classes);
+		return labeled;
+	}
+}

+ 50 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/enums/FABVariants.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxcomponents.theming.enums;
+
+import io.github.palexdev.mfxcomponents.controls.fab.MFXExtendedFab;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFab;
+import io.github.palexdev.mfxcomponents.theming.base.Variant;
+
+/**
+ * Enumerator implementing {@link Variant} to define the variants of {@link MFXFab} and {@link MFXExtendedFab}.
+ * <p></p>
+ * Note that {@link MFXExtendedFab} doesn't have 'small' and 'large' variants, applying those will likely result
+ * in un-styled components.
+ */
+public enum FABVariants implements Variant {
+	SMALL("small"),
+	LARGE("large"),
+	LOWERED("lowered"),
+	SURFACE("surface"),
+	SECONDARY("secondary"),
+	TERTIARY("tertiary"),
+	;
+
+	private final String styleClass;
+
+	FABVariants(String styleClass) {
+		this.styleClass = styleClass;
+	}
+
+	@Override
+	public String variantStyleClass() {
+		return styleClass;
+	}
+}

+ 1 - 1
modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/PseudoClasses.java → modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/enums/PseudoClasses.java

@@ -16,7 +16,7 @@
  * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.mfxcomponents.theming;
+package io.github.palexdev.mfxcomponents.theming.enums;
 
 import javafx.css.PseudoClass;
 import javafx.scene.Node;

+ 4 - 1
modules/components/src/main/java/module-info.java

@@ -10,11 +10,14 @@ module mfx.components {
 	exports io.github.palexdev.mfxcomponents.behaviors;
 
 	// Controls
+	exports io.github.palexdev.mfxcomponents.controls.base;
 	exports io.github.palexdev.mfxcomponents.controls.buttons;
+	exports io.github.palexdev.mfxcomponents.controls.fab;
 
 	// Skins
 	exports io.github.palexdev.mfxcomponents.skins;
 
 	// Theming
-	exports io.github.palexdev.mfxcomponents.theming;
+	exports io.github.palexdev.mfxcomponents.theming.base;
+	exports io.github.palexdev.mfxcomponents.theming.enums;
 }

+ 121 - 14
modules/components/src/test/java/app/buttons/ButtonsPlayground.java

@@ -22,8 +22,12 @@ import app.others.ui.MultipleViewApp;
 import app.others.ui.TitledFlowPane;
 import app.others.ui.ViewSwitcher;
 import io.github.palexdev.mfxcomponents.controls.buttons.*;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXExtendedFab;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFab;
+import io.github.palexdev.mfxcomponents.theming.enums.FABVariants;
+import io.github.palexdev.mfxcore.builders.InsetsBuilder;
 import io.github.palexdev.mfxresources.MFXResources;
-import io.github.palexdev.mfxresources.fonts.IconsProviders;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
 import javafx.application.Application;
 import javafx.collections.FXCollections;
 import javafx.css.PseudoClass;
@@ -33,13 +37,15 @@ import javafx.scene.Node;
 import javafx.scene.Scene;
 import javafx.scene.control.ComboBox;
 import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.ScrollPane;
 import javafx.scene.layout.BorderPane;
-import javafx.scene.paint.Color;
+import javafx.scene.layout.VBox;
 import javafx.stage.Stage;
 
 import java.util.function.BiFunction;
 
 import static io.github.palexdev.mfxresources.fonts.IconsProviders.FONTAWESOME_SOLID;
+import static io.github.palexdev.mfxresources.fonts.IconsProviders.randomIcon;
 
 public class ButtonsPlayground extends Application implements MultipleViewApp<String> {
 	//================================================================================
@@ -64,7 +70,11 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 
 		header.getSelectionModel().selectFirst();
 
-		Scene scene = new Scene(root, 800, 800);
+		ScrollPane sp = new ScrollPane(root);
+		sp.setFitToWidth(true);
+		sp.setFitToHeight(true);
+
+		Scene scene = new Scene(sp, 800, 800);
 		loadStyleSheet(scene);
 		stage.setScene(scene);
 		stage.setTitle("Buttons Playground");
@@ -80,6 +90,8 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		switcher.register("tonal-filled-buttons", s -> tfbView());
 		switcher.register("outlined-buttons", s -> obView());
 		switcher.register("text-buttons", s -> tbView());
+		switcher.register("fabs", s -> fabView());
+		switcher.register("extended-fabs", s -> extendedFabView());
 	}
 
 	@Override
@@ -97,31 +109,55 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 	//================================================================================
 
 	private Node ebView() {
-		return createButtonsView(MFXElevatedButton::new);
+		return createButtonsView("Elevated Buttons", MFXElevatedButton::new);
 	}
 
 	private Node fbView() {
-		return createButtonsView(MFXFilledButton::new);
+		return createButtonsView("Filled Buttons", MFXFilledButton::new);
 	}
 
 	private Node tfbView() {
-		return createButtonsView(MFXTonalFilledButton::new);
+		return createButtonsView("Tonal Filled Buttons", MFXTonalFilledButton::new);
 	}
 
 	private Node obView() {
-		return createButtonsView(MFXOutlinedButton::new);
+		return createButtonsView("Outlined Buttons", MFXOutlinedButton::new);
 	}
 
 	private Node tbView() {
-		return createButtonsView(600, MFXTextButton::new);
+		return createButtonsView("Text Buttons", 600, MFXTextButton::new);
+	}
+
+	private Node fabView() {
+		VBox box = new VBox(50);
+		box.setAlignment(Pos.TOP_CENTER);
+		box.setPadding(InsetsBuilder.all(10));
+		Node def = createFabsView("Floating Action Buttons", (s, i) -> new MFXFab(i));
+		Node surf = createFabsView("Floating Action Buttons (Surface)", (s, i) -> new MFXFab(i).setVariants(FABVariants.SURFACE));
+		Node sdy = createFabsView("Floating Action Buttons (Secondary)", (s, i) -> new MFXFab(i).setVariants(FABVariants.SECONDARY));
+		Node tty = createFabsView("Floating Action Buttons (Tertiary)", (s, i) -> new MFXFab(i).setVariants(FABVariants.TERTIARY));
+		box.getChildren().addAll(def, surf, sdy, tty);
+		return box;
+	}
+
+	private Node extendedFabView() {
+		VBox box = new VBox(50);
+		box.setAlignment(Pos.TOP_CENTER);
+		box.setPadding(InsetsBuilder.all(10));
+		Node def = createExtendedFabView("Extended FABs", MFXExtendedFab::new);
+		Node surf = createExtendedFabView("Extended FABs (Surface)", (s, i) -> new MFXExtendedFab(s, i).setVariants(FABVariants.SURFACE));
+		Node sdy = createExtendedFabView("Extended FABs (Secondary)", (s, i) -> new MFXExtendedFab(s, i).setVariants(FABVariants.SECONDARY));
+		Node tty = createExtendedFabView("Extended FABs (Tertiary)", (s, i) -> new MFXExtendedFab(s, i).setVariants(FABVariants.TERTIARY));
+		box.getChildren().addAll(def, surf, sdy, tty);
+		return box;
 	}
 
-	private Node createButtonsView(BiFunction<String, Node, MFXButton> generator) {
-		return createButtonsView(700, generator);
+	private Node createButtonsView(String title, BiFunction<String, Node, MFXButton> generator) {
+		return createButtonsView(title, 700, generator);
 	}
 
-	private Node createButtonsView(double length, BiFunction<String, Node, MFXButton> generator) {
-		TitledFlowPane tfp = new TitledFlowPane("Filled Buttons");
+	private Node createButtonsView(String title, double length, BiFunction<String, Node, MFXButton> generator) {
+		TitledFlowPane tfp = new TitledFlowPane(title);
 		tfp.setMaxWidth(length);
 
 		MFXButton btn0 = generator.apply("Enabled", null);
@@ -129,8 +165,8 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		MFXButton btn2 = generator.apply("Hovered", null);
 		MFXButton btn3 = generator.apply("Focused", null);
 		MFXButton btn4 = generator.apply("Pressed", null);
-		MFXButton btn5 = generator.apply("Icon Left", IconsProviders.randomIcon(FONTAWESOME_SOLID, 24.0, Color.TRANSPARENT));
-		MFXButton btn6 = generator.apply("Icon Right", IconsProviders.randomIcon(FONTAWESOME_SOLID, 24.0, Color.TRANSPARENT));
+		MFXButton btn5 = generator.apply("Icon Left", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn6 = generator.apply("Icon Right", randomIcon(FONTAWESOME_SOLID));
 		btn6.setContentDisplay(ContentDisplay.RIGHT);
 
 		btn1.setDisable(true);
@@ -144,4 +180,75 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		tfp.add(btn0, btn1, btn2, btn3, btn4, btn5, btn6);
 		return tfp;
 	}
+
+	private Node createFabsView(String title, BiFunction<String, MFXFontIcon, MFXFab> generator) {
+		return createFabsView(title, 700, generator);
+	}
+
+	private Node createFabsView(String title, double length, BiFunction<String, MFXFontIcon, MFXFab> generator) {
+		TitledFlowPane defTfp = new TitledFlowPane(title);
+		defTfp.setMaxWidth(length);
+
+		MFXButton btn0 = generator.apply("Enabled", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn1 = generator.apply("Disabled", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn2 = generator.apply("Hovered", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn3 = generator.apply("Focused", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn4 = generator.apply("Pressed", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn5 = generator.apply("Small", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn6 = generator.apply("Large", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn7 = generator.apply("Large Lowered", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn8 = generator.apply("Lowered Large", randomIcon(FONTAWESOME_SOLID));
+
+		btn1.setDisable(true);
+		btn2.setMouseTransparent(true);
+		btn2.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
+		btn3.setMouseTransparent(true);
+		btn3.pseudoClassStateChanged(PseudoClass.getPseudoClass("focused"), true);
+		btn4.setMouseTransparent(true);
+		btn4.pseudoClassStateChanged(PseudoClass.getPseudoClass("pressed"), true);
+		btn5.addVariants(FABVariants.SMALL);
+		btn6.addVariants(FABVariants.LARGE);
+		btn7.addVariants(FABVariants.LARGE, FABVariants.LOWERED);
+		btn8.addVariants(FABVariants.LOWERED, FABVariants.LARGE);
+
+		defTfp.add(btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8);
+		return defTfp;
+	}
+
+	private Node createExtendedFabView(String title, BiFunction<String, MFXFontIcon, MFXExtendedFab> generator) {
+		return createExtendedFabView(title, 700, generator);
+	}
+
+	private Node createExtendedFabView(String title, double length, BiFunction<String, MFXFontIcon, MFXExtendedFab> generator) {
+		TitledFlowPane defTfp = new TitledFlowPane(title);
+		defTfp.setMaxWidth(length);
+
+		MFXButton btn0 = generator.apply("Enabled", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn1 = generator.apply("Disabled", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn2 = generator.apply("Hovered", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn3 = generator.apply("Focused", randomIcon(FONTAWESOME_SOLID));
+		MFXButton btn4 = generator.apply("Pressed", randomIcon(FONTAWESOME_SOLID));
+		MFXExtendedFab btn5 = generator.apply("Text Only", randomIcon(FONTAWESOME_SOLID));
+		MFXExtendedFab btn6 = generator.apply("Icon to Right", randomIcon(FONTAWESOME_SOLID));
+		MFXExtendedFab btn7 = generator.apply("Lowered Text Only", randomIcon(FONTAWESOME_SOLID));
+		MFXExtendedFab btn8 = generator.apply("Lowered Icon to Right", randomIcon(FONTAWESOME_SOLID));
+
+		btn1.setDisable(true);
+		btn2.setMouseTransparent(true);
+		btn2.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
+		btn3.setMouseTransparent(true);
+		btn3.pseudoClassStateChanged(PseudoClass.getPseudoClass("focused"), true);
+		btn4.setMouseTransparent(true);
+		btn4.pseudoClassStateChanged(PseudoClass.getPseudoClass("pressed"), true);
+
+		btn5.setContentDisplay(ContentDisplay.TEXT_ONLY);
+		btn6.setContentDisplay(ContentDisplay.RIGHT);
+		btn7.addVariants(FABVariants.LOWERED);
+		btn7.setContentDisplay(ContentDisplay.TEXT_ONLY);
+		btn8.addVariants(FABVariants.LOWERED);
+		btn8.setContentDisplay(ContentDisplay.RIGHT);
+
+		defTfp.add(btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8);
+		return defTfp;
+	}
 }

+ 124 - 0
modules/components/src/test/java/interactive/TestInitSize.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package interactive;
+
+import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton;
+import io.github.palexdev.mfxcomponents.controls.buttons.MFXFilledButton;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFab;
+import io.github.palexdev.mfxcomponents.theming.enums.FABVariants;
+import io.github.palexdev.mfxresources.MFXResources;
+import javafx.scene.Scene;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testfx.api.FxRobot;
+import org.testfx.api.FxToolkit;
+import org.testfx.framework.junit5.ApplicationExtension;
+import org.testfx.framework.junit5.Start;
+
+import java.util.concurrent.TimeoutException;
+
+import static io.github.palexdev.mfxresources.fonts.IconsProviders.FONTAWESOME_SOLID;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(ApplicationExtension.class)
+public class TestInitSize {
+	private static Stage stage;
+
+	@Start
+	void start(Stage stage) {
+		TestInitSize.stage = stage;
+		stage.show();
+	}
+
+	@Test
+	void testStandardFab(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = new MFXFab(FONTAWESOME_SOLID.randomIcon());
+		robot.interact(() -> root.getChildren().setAll(fab));
+		assertEquals(56, fab.getLayoutBounds().getWidth());
+		assertEquals(56, fab.getLayoutBounds().getHeight());
+
+		robot.interact(() -> fab.setPrefSize(70, 70));
+		assertEquals(70, fab.getLayoutBounds().getWidth());
+		assertEquals(70, fab.getLayoutBounds().getHeight());
+	}
+
+	@Test
+	void testSmallFab(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = new MFXFab(FONTAWESOME_SOLID.randomIcon());
+		robot.interact(() -> root.getChildren().setAll(fab));
+		assertEquals(56, fab.getLayoutBounds().getWidth());
+		assertEquals(56, fab.getLayoutBounds().getHeight());
+
+		robot.interact(() -> fab.setVariants(FABVariants.SMALL));
+		assertEquals(40, fab.getLayoutBounds().getWidth());
+		assertEquals(40, fab.getLayoutBounds().getHeight());
+
+		robot.interact(() -> fab.setPrefSize(70, 70));
+		assertEquals(70, fab.getLayoutBounds().getWidth());
+		assertEquals(70, fab.getLayoutBounds().getHeight());
+	}
+
+	@Test
+	void testLargeFab(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = new MFXFab(FONTAWESOME_SOLID.randomIcon());
+		robot.interact(() -> root.getChildren().setAll(fab));
+		assertEquals(56, fab.getLayoutBounds().getWidth());
+		assertEquals(56, fab.getLayoutBounds().getHeight());
+
+		robot.interact(() -> fab.setVariants(FABVariants.LARGE));
+		assertEquals(96, fab.getLayoutBounds().getWidth());
+		assertEquals(96, fab.getLayoutBounds().getHeight());
+
+		robot.interact(() -> fab.setPrefSize(70, 70));
+		assertEquals(70, fab.getLayoutBounds().getWidth());
+		assertEquals(70, fab.getLayoutBounds().getHeight());
+	}
+
+	@Test
+	@Disabled
+	void testButtons(FxRobot robot) {
+		// For some reason this test will always succeed even without
+		// using -mfx-init-height in the stylesheet. However, the sizing will
+		// result broken in SceneBuilder
+		StackPane root = setupStage();
+		MFXFilledButton btn = MFXButton.filled();
+		robot.interact(() -> root.getChildren().add(btn));
+		assertEquals(40, btn.getLayoutBounds().getHeight());
+
+		robot.interact(() -> btn.setPrefHeight(100));
+		assertEquals(100, btn.getLayoutBounds().getHeight());
+	}
+
+	private StackPane setupStage() {
+		try {
+			Scene scene = new Scene(new StackPane(), 200, 200);
+			scene.getStylesheets().add(MFXResources.load("sass/md3/mfx-light.css")); // TODO change once Theme Manager API is done
+			FxToolkit.setupStage(s -> s.setScene(scene));
+		} catch (TimeoutException e) {
+			throw new RuntimeException(e);
+		}
+		return ((StackPane) stage.getScene().getRoot());
+	}
+}

+ 42 - 0
modules/components/src/test/java/interactive/TestVariants.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package interactive;
+
+import javafx.stage.Stage;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testfx.api.FxRobot;
+import org.testfx.framework.junit5.ApplicationExtension;
+import org.testfx.framework.junit5.Start;
+
+@ExtendWith(ApplicationExtension.class)
+public class TestVariants {
+	private static Stage stage;
+
+	@Start
+	void start(Stage stage) {
+		TestVariants.stage = stage;
+		stage.show();
+	}
+
+	@Test
+	void testVariantsCommutativity(FxRobot robot) {
+
+	}
+}

+ 20 - 0
modules/core/src/main/java/io/github/palexdev/mfxcore/utils/fx/SceneBuilderIntegration.java

@@ -18,8 +18,12 @@
 
 package io.github.palexdev.mfxcore.utils.fx;
 
+import java.io.IOException;
 import java.lang.StackWalker.Option;
 import java.lang.StackWalker.StackFrame;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.Set;
 
 /**
@@ -45,6 +49,7 @@ public class SceneBuilderIntegration {
 	private static Boolean inSceneBuilder = null;
 	private static int depth = 10;
 	private static boolean isDepthInvalid = false;
+	public static final Path DEFAULT_DEBUG_FILE = Path.of(System.getProperty("user.home") + "/SceneBuilderIntegrationDebug.log");
 
 	//================================================================================
 	// Constructors
@@ -93,4 +98,19 @@ public class SceneBuilderIntegration {
 		SceneBuilderIntegration.depth = depth;
 		SceneBuilderIntegration.isDepthInvalid = true;
 	}
+
+	/**
+	 * Creates a new file at the given {@link Path} if non-existent, then writes the given debug String
+	 * in it. The 'truncate' boolean flag specifies whether to append the string or clear the file content first.
+	 * <p></p>
+	 * For the path you can also use {@link #DEFAULT_DEBUG_FILE} which points at the 'user.home' directory, and
+	 * creates a 'SceneBuilderIntegrationDebug.log' file in it.
+	 */
+	public static void debug(Path file, boolean truncate, String debug) {
+		try {
+			StandardOpenOption wrOpt = truncate ? StandardOpenOption.TRUNCATE_EXISTING : StandardOpenOption.APPEND;
+			Files.writeString(file, debug + "\n", StandardOpenOption.CREATE, StandardOpenOption.WRITE, wrOpt);
+		} catch (IOException ignored) {
+		}
+	}
 }

+ 4 - 4
modules/effects/src/main/java/io/github/palexdev/mfxeffects/enums/ElevationLevel.java

@@ -101,10 +101,10 @@ public enum ElevationLevel {
 	public void animateTo(DropShadow current, ElevationLevel next) {
 		Interpolator i = BezierEasing.EASE;
 		TimelineBuilder.build()
-				.add(KeyFrames.of(50, current.offsetXProperty(), next.getOffsetX(), i))
-				.add(KeyFrames.of(50, current.offsetYProperty(), next.getOffsetY(), i))
-				.add(KeyFrames.of(50, current.radiusProperty(), next.getRadius(), i))
-				.add(KeyFrames.of(50, current.spreadProperty(), next.getSpread(), i))
+				.add(KeyFrames.of(1, current.offsetXProperty(), next.getOffsetX(), i))
+				.add(KeyFrames.of(1, current.offsetYProperty(), next.getOffsetY(), i))
+				.add(KeyFrames.of(250, current.radiusProperty(), next.getRadius(), i))
+				.add(KeyFrames.of(250, current.spreadProperty(), next.getSpread(), i))
 				.getAnimation().play();
 	}
 

+ 120 - 0
modules/resources/src/main/java/io/github/palexdev/mfxresources/base/properties/IconProperty.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxresources.base.properties;
+
+import io.github.palexdev.mfxresources.fonts.IconProvider;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.scene.text.Font;
+
+import java.util.function.Function;
+
+/**
+ * Simple extension of {@link ReadOnlyObjectWrapper} to be used for {@link MFXFontIcon} objects,
+ * also offers a series of convenient methods to manipulate the icon with fluent API.
+ */
+public class IconProperty extends ReadOnlyObjectWrapper<MFXFontIcon> {
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public IconProperty() {
+	}
+
+	public IconProperty(MFXFontIcon initialValue) {
+		super(initialValue);
+	}
+
+	public IconProperty(Object bean, String name) {
+		super(bean, name);
+	}
+
+	public IconProperty(Object bean, String name, MFXFontIcon initialValue) {
+		super(bean, name, initialValue);
+	}
+
+	//================================================================================
+	// Setters
+	//================================================================================
+
+	/**
+	 * Changes the {@link MFXFontIcon#descriptionProperty()} of the current value.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXFontIcon} will
+	 * be created and no exception will be raised.
+	 */
+	public IconProperty setDescription(String description) {
+		MFXFontIcon val = get();
+		if (val == null) {
+			set(new MFXFontIcon(description));
+		} else {
+			val.setDescription(description);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXFontIcon#setIconsProvider(IconProvider)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXFontIcon} will
+	 * be created and no exception will be raised.
+	 */
+	public IconProperty setProvider(IconProvider provider) {
+		MFXFontIcon val = get();
+		if (val == null) {
+			set(new MFXFontIcon().setIconsProvider(provider));
+		} else {
+			val.setIconsProvider(provider);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXFontIcon#setIconsProvider(Font, Function)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXFontIcon} will
+	 * be created and no exception will be raised.
+	 */
+	public IconProperty setProvider(Font font, Function<String, Character> converter) {
+		MFXFontIcon val = get();
+		if (val == null) {
+			set(new MFXFontIcon().setIconsProvider(font, converter));
+		} else {
+			val.setIconsProvider(font, converter);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXFontIcon#setIconsProvider(IconProvider)}, additionally
+	 * also changes the {@link MFXFontIcon#descriptionProperty()} to the given one.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXFontIcon} will
+	 * be created and no exception will be raised.
+	 */
+	public IconProperty setProvider(IconProvider provider, String description) {
+		MFXFontIcon val = get();
+		if (val == null) {
+			set(new MFXFontIcon().setIconsProvider(provider).setDescription(description));
+		} else {
+			val.setIconsProvider(provider).setDescription(description);
+		}
+		return this;
+	}
+}

+ 152 - 0
modules/resources/src/main/java/io/github/palexdev/mfxresources/base/properties/WrappedIconProperty.java

@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.mfxresources.base.properties;
+
+import io.github.palexdev.mfxeffects.beans.Position;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import io.github.palexdev.mfxresources.fonts.MFXIconWrapper;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.scene.input.MouseEvent;
+
+import java.util.function.Function;
+
+/**
+ * Simple extension of {@link ReadOnlyObjectWrapper} to be used for {@link MFXIconWrapper} objects,
+ * also offers a series of convenient methods to manipulate the icon with fluent API.
+ */
+public class WrappedIconProperty extends ReadOnlyObjectWrapper<MFXIconWrapper> {
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public WrappedIconProperty() {
+	}
+
+	public WrappedIconProperty(MFXIconWrapper initialValue) {
+		super(initialValue);
+	}
+
+	public WrappedIconProperty(Object bean, String name) {
+		super(bean, name);
+	}
+
+	public WrappedIconProperty(Object bean, String name, MFXIconWrapper initialValue) {
+		super(bean, name, initialValue);
+	}
+
+	//================================================================================
+	// Setters
+	//================================================================================
+
+	/**
+	 * Sets the {@link MFXIconWrapper#iconProperty()} to the given {@link MFXFontIcon}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXIconWrapper} will
+	 * be created and no exception will be raised.
+	 */
+	public WrappedIconProperty setIcon(MFXFontIcon icon) {
+		MFXIconWrapper val = get();
+		if (val == null) {
+			set(new MFXIconWrapper(icon));
+		} else {
+			val.setIcon(icon);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXIconWrapper#enableRippleGenerator(boolean)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXIconWrapper} will
+	 * be created and no exception will be raised.
+	 */
+	public WrappedIconProperty enableRippleGenerator(boolean enable) {
+		MFXIconWrapper val = get();
+		if (val == null) {
+			set(new MFXIconWrapper().enableRippleGenerator(enable));
+		} else {
+			val.enableRippleGenerator(enable);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXIconWrapper#enableRippleGenerator(boolean, Function)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXIconWrapper} will
+	 * be created and no exception will be raised.
+	 */
+	public WrappedIconProperty enableRippleGenerator(boolean enable, Function<MouseEvent, Position> positionFunction) {
+		MFXIconWrapper val = get();
+		if (val == null) {
+			set(new MFXIconWrapper().enableRippleGenerator(enable, positionFunction));
+		} else {
+			val.enableRippleGenerator(enable, positionFunction);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXIconWrapper#makeRound(boolean)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXIconWrapper} will
+	 * be created and no exception will be raised.
+	 */
+	public WrappedIconProperty makeRound(boolean state) {
+		MFXIconWrapper val = get();
+		if (val == null) {
+			set(new MFXIconWrapper().makeRound(state));
+		} else {
+			val.makeRound(state);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXIconWrapper#makeRound(boolean, double)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXIconWrapper} will
+	 * be created and no exception will be raised.
+	 */
+	public WrappedIconProperty makeRound(boolean state, double radius) {
+		MFXIconWrapper val = get();
+		if (val == null) {
+			set(new MFXIconWrapper().makeRound(state, radius));
+		} else {
+			val.makeRound(state, radius);
+		}
+		return this;
+	}
+
+	/**
+	 * Delegate for {@link MFXIconWrapper#setSize(double)}.
+	 * <p>
+	 * This is null-safe, meaning that if the current value of the property is null a new {@link MFXIconWrapper} will
+	 * be created and no exception will be raised.
+	 */
+	public WrappedIconProperty setSize(double size) {
+		MFXIconWrapper val = get();
+		if (val == null) {
+			set(new MFXIconWrapper(null, size));
+		} else {
+			val.setSize(size);
+		}
+		return this;
+	}
+}

+ 37 - 0
modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/IconsProviders.java

@@ -95,6 +95,36 @@ public enum IconsProviders implements IconProvider {
 		return icon;
 	}
 
+	/**
+	 * Creates a new {@link MFXFontIcon} with a random icon description extracted from the values of "this" enumerator
+	 * constant.
+	 */
+	public MFXFontIcon randomIcon() {
+		MFXFontIcon icon = new MFXFontIcon();
+		String desc;
+		switch (this) {
+			case FONTAWESOME_BRANDS: {
+				icon.setIconsProvider(FONTAWESOME_BRANDS);
+				desc = EnumUtils.randomEnum(FontAwesomeBrands.class).getDescription();
+				break;
+			}
+			case FONTAWESOME_REGULAR: {
+				icon.setIconsProvider(FONTAWESOME_REGULAR);
+				desc = EnumUtils.randomEnum(FontAwesomeRegular.class).getDescription();
+				break;
+			}
+			case FONTAWESOME_SOLID: {
+				icon.setIconsProvider(FONTAWESOME_SOLID);
+				desc = EnumUtils.randomEnum(FontAwesomeSolid.class).getDescription();
+				break;
+			}
+			default:
+				return icon;
+		}
+		icon.setDescription(desc);
+		return icon;
+	}
+
 	/**
 	 * Same as {@link #randomIcon(double, Color)}, allows usage from a static context.
 	 */
@@ -102,6 +132,13 @@ public enum IconsProviders implements IconProvider {
 		return provider.randomIcon(size, color);
 	}
 
+	/**
+	 * Same as {@link #randomIcon()}, allows usage from a static context.
+	 */
+	public static MFXFontIcon randomIcon(IconsProviders provider) {
+		return provider.randomIcon();
+	}
+
 	/**
 	 * @return the default icon provider used by {@link MFXFontIcon}s, currently {@link #FONTAWESOME_SOLID}
 	 */

+ 3 - 4
modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java

@@ -20,8 +20,7 @@ package io.github.palexdev.mfxresources.fonts;
 
 import io.github.palexdev.mfxeffects.beans.Position;
 import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
+import io.github.palexdev.mfxresources.base.properties.IconProperty;
 import javafx.collections.ObservableList;
 import javafx.css.*;
 import javafx.event.EventHandler;
@@ -58,7 +57,7 @@ public class MFXIconWrapper extends StackPane {
 	//================================================================================
 	private final String STYLE_CLASS = "mfx-icon-wrapper";
 
-	private final ObjectProperty<MFXFontIcon> icon = new SimpleObjectProperty<>();
+	private final IconProperty icon = new IconProperty();
 	private MFXRippleGenerator rg;
 	private EventHandler<MouseEvent> rHandler;
 
@@ -404,7 +403,7 @@ public class MFXIconWrapper extends StackPane {
 	/**
 	 * Specifies the currently contained {@link MFXFontIcon}.
 	 */
-	public ObjectProperty<MFXFontIcon> iconProperty() {
+	public IconProperty iconProperty() {
 		return icon;
 	}
 

+ 4 - 1
modules/resources/src/main/java/module-info.java

@@ -3,9 +3,12 @@ module mfx.resources {
 
 	requires transitive javafx.graphics;
 
-	// Base
+	// Root
 	exports io.github.palexdev.mfxresources;
 
+	// Base
+	exports io.github.palexdev.mfxresources.base.properties;
+
 	// Builders
 	exports io.github.palexdev.mfxresources.builders;
 

+ 4 - 2
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_buttons.scss

@@ -1,5 +1,7 @@
 @use 'elevated_button';
 @use 'filled_button';
-@use 'tonal_filled_button';
 @use 'outlined_button';
-@use 'text_button';
+@use 'text_button';
+@use 'tonal_filled_button';
+@use "fab";
+@use "extended_fab";

+ 2 - 3
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_buttons_base.scss

@@ -7,7 +7,7 @@ $padding: 0px 24px 0px 24px !default;
 $padding-i-left: 0px 24px 0px 16px !default;
 $padding-i-right: 0px 16px 0px 24px !default;
 
-@mixin base($surface, $onSurface, $text, $surfaceTint, $elevation) {
+@mixin base($surface, $surfaceTint, $text, $elevation) {
   @include label_large();
   -fx-text-fill: get_scheme_color($text);
   @if (type-of($surface) == color) {
@@ -16,9 +16,9 @@ $padding-i-right: 0px 16px 0px 24px !default;
     -fx-background-color: elevate($surface, $surfaceTint, $elevation);
   }
   -fx-background-radius: 20px;
-  -fx-pref-height: 40px;
   -fx-padding: $padding;
   -fx-graphic-text-gap: 8px;
+  -mfx-init-height: 40px;
   -mfx-elevation: unquote("LEVEL" + $elevation);
 }
 
@@ -81,7 +81,6 @@ $padding-i-right: 0px 16px 0px 24px !default;
   &:with-icon-right {
     -fx-padding: $padding-i-right;
   }
-
 }
 
 @mixin ripple($color) {

+ 1 - 2
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_elevated_button.scss

@@ -4,14 +4,13 @@
  * Elevated Buttons
  ****************************************************************************************************/
 $surface: 'surface';
-$onSurface: 'on-surface';
 $surfaceTint: 'surface-tint';
 $text: 'primary';
 $icon: 'primary';
 $ripple: 'primary';
 
 .mfx-button.elevated {
-  @include buttons_base.base($surface, $onSurface, $text, $surfaceTint, 1);
+  @include buttons_base.base($surface, $surfaceTint, $text, 1);
   @include buttons_base.disabled();
   @include buttons_base.hover($surface, $surfaceTint, 2);
   @include buttons_base.focus_press($surface, $surfaceTint, 1);

+ 143 - 0
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_extended_fab.scss

@@ -0,0 +1,143 @@
+@use 'buttons_base';
+@use '../../abstracts/elevation_utils' as *;
+@use '../../abstracts/theme_utils' as *;
+@use '../../tokens/typography' as *;
+
+// Base mixins, these are different from common buttons base
+@mixin fab_base($surface, $surfaceTint, $text, $elevation) {
+  @include label_large();
+  -fx-text-fill: get_scheme_color($text);
+  -fx-background-color: get_scheme_color($surface);
+  -fx-background-radius: 16px;
+  -mfx-init-height: 56px;
+  -mfx-init-width: 80px;
+  -fx-graphic-text-gap: 8px;
+  -mfx-elevation: unquote("LEVEL" + $elevation);
+}
+
+@mixin fab_lowered($def_elevation, $hover_elevation, $focus_press_elevation) {
+  &.lowered {
+    -mfx-elevation: unquote("LEVEL" + $def_elevation);
+  }
+
+  &.lowered:hover {
+    -mfx-elevation: unquote("LEVEL" + $hover_elevation);
+  }
+
+  &.lowered:focused,
+  &.lowered:pressed {
+    -mfx-elevation: unquote("LEVEL" + $focus_press_elevation);
+  }
+}
+
+@mixin fab_disabled() {
+  &:disabled > .label {
+    -fx-opacity: 1.0;
+  }
+}
+
+@mixin fab_hover($surface, $surfaceTint, $elevation) {
+  &:hover {
+    -fx-background-color: state_layer(get_scheme_color($surface), $surfaceTint, 'hover');
+    -mfx-elevation: unquote("LEVEL" + $elevation);
+  }
+}
+
+@mixin fab_focus_press($surface, $surfaceTint, $elevation) {
+  &:focused,
+  &:pressed {
+    -fx-background-color: state_layer(get_scheme_color($surface), $surfaceTint, 'focused');
+    -mfx-elevation: unquote("LEVEL" + $elevation);
+  }
+}
+
+@mixin fab_icon($color) {
+  &:with-icon-left {
+    -fx-padding: 0px 20px 0px 16px;
+  }
+
+  &:with-icon-right {
+    -fx-padding: 0px 16px 0px 20px;
+  }
+
+  .mfx-font-icon {
+    -mfx-color: get_scheme_color($color);
+    -mfx-size: 24px;
+  }
+}
+
+/****************************************************************************************************
+ * Extended FAB Primary
+ ****************************************************************************************************/
+$pmy_surface: 'primary-container';
+$pmy_tint: 'on-primary-container';
+$pmy_text: 'on-primary-container';
+$pmy_icon: 'on-primary-container';
+$pmy_ripple: 'on-primary-container';
+
+.mfx-button.extended-fab {
+  @include fab_base($pmy_surface, $pmy_tint, $pmy_text, 3);
+  @include fab_disabled();
+  @include fab_hover($pmy_surface, $pmy_tint, 4);
+  @include fab_focus_press($pmy_surface, $pmy_tint, 3);
+  @include fab_icon($pmy_icon);
+  @include buttons_base.ripple($pmy_ripple);
+  @include fab_lowered(1, 2, 1);
+}
+
+/****************************************************************************************************
+ * Extended FAB Surface
+ ****************************************************************************************************/
+$sfe_surface: 'surface';
+$sfe_tint: 'primary';
+$sfe_text: 'primary';
+$sfe_icon: 'primary';
+$sfe_ripple: 'primary';
+
+.mfx-button.extended-fab.surface {
+  @include fab_base($sfe_surface, $sfe_tint, $sfe_text, 3);
+  @include fab_disabled();
+  @include fab_hover($sfe_surface, $sfe_tint, 4);
+  @include fab_focus_press($sfe_surface, $sfe_tint, 3);
+  @include fab_icon($sfe_icon);
+  @include buttons_base.ripple($sfe_ripple);
+  @include fab_lowered(1, 2, 1);
+}
+
+/****************************************************************************************************
+ * Extended FAB Secondary
+ ****************************************************************************************************/
+$sdy_surface: 'secondary-container';
+$sdy_tint: 'on-secondary-container';
+$sdy_text: 'on-secondary-container';
+$sdy_icon: 'on-secondary-container';
+$sdy_ripple: 'on-secondary-container';
+
+.mfx-button.extended-fab.secondary {
+  @include fab_base($sdy_surface, $sdy_tint, $sdy_text, 3);
+  @include fab_disabled();
+  @include fab_hover($sdy_surface, $sdy_tint, 4);
+  @include fab_focus_press($sdy_surface, $sdy_tint, 3);
+  @include fab_icon($sdy_icon);
+  @include buttons_base.ripple($sdy_ripple);
+  @include fab_lowered(1, 2, 1);
+}
+
+/****************************************************************************************************
+ * Extended FAB Tertiary
+ ****************************************************************************************************/
+$tty_surface: 'tertiary-container';
+$tty_tint: 'on-tertiary-container';
+$tty_text: 'on-tertiary-container';
+$tty_icon: 'on-tertiary-container';
+$tty_ripple: 'on-tertiary-container';
+
+.mfx-button.extended-fab.tertiary {
+  @include fab_base($tty_surface, $tty_tint, $tty_text, 3);
+  @include fab_disabled();
+  @include fab_hover($tty_surface, $tty_tint, 4);
+  @include fab_focus_press($tty_surface, $tty_tint, 3);
+  @include fab_icon($tty_icon);
+  @include buttons_base.ripple($tty_ripple);
+  @include fab_lowered(1, 2, 1);
+}

+ 155 - 0
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_fab.scss

@@ -0,0 +1,155 @@
+@use 'buttons_base';
+@use '../../abstracts/elevation_utils' as *;
+@use '../../abstracts/theme_utils' as *;
+
+// Base mixins, these are different from common buttons base
+@mixin fab_base($surface, $surfaceTint, $elevation) {
+  -fx-background-color: get_scheme_color($surface);
+  -fx-background-radius: 16px;
+  -mfx-init-height: 56px;
+  -mfx-init-width: 56px;
+  -mfx-elevation: unquote("LEVEL" + $elevation);
+
+  &.small {
+    -fx-background-radius: 12px;
+    -mfx-init-height: 40px;
+    -mfx-init-width: 40px;
+  }
+
+  &.large {
+    -fx-background-radius: 28px;
+    -mfx-init-height: 96px;
+    -mfx-init-width: 96px;
+  }
+}
+
+@mixin fab_lowered($def_elevation, $hover_elevation, $focus_press_elevation) {
+  &.lowered {
+    -mfx-elevation: unquote("LEVEL" + $def_elevation);
+  }
+
+  &.lowered:hover {
+    -mfx-elevation: unquote("LEVEL" + $hover_elevation);
+  }
+
+  &.lowered:focused,
+  &.lowered:pressed {
+    -mfx-elevation: unquote("LEVEL" + $focus_press_elevation);
+  }
+}
+
+@mixin fab_disabled() {
+  &:disabled > .label {
+    -fx-opacity: 1.0;
+  }
+}
+
+@mixin fab_hover($surface, $surfaceTint, $elevation) {
+  &:hover {
+    -fx-background-color: state_layer(get_scheme_color($surface), $surfaceTint, 'hover');
+    -mfx-elevation: unquote("LEVEL" + $elevation);
+  }
+}
+
+@mixin fab_focus_press($surface, $surfaceTint, $elevation) {
+  &:focused,
+  &:pressed {
+    -fx-background-color: state_layer(get_scheme_color($surface), $surfaceTint, 'focused');
+    -mfx-elevation: unquote("LEVEL" + $elevation);
+  }
+}
+
+@mixin fab_icon($color) {
+  $s_size: 24px;
+  $m_size: 24px;
+  $l_size: 36px;
+
+  .mfx-font-icon {
+    -mfx-color: get_scheme_color($color);
+    -mfx-size: $m_size;
+  }
+
+  &.small .mfx-font-icon {
+    -mfx-size: $s_size;
+  }
+
+  &.large .mfx-font-icon {
+    -mfx-size: $l_size;
+  }
+}
+
+/****************************************************************************************************
+ * FAB Primary
+ ****************************************************************************************************/
+$pmy_surface: 'primary-container';
+$pmy_tint: 'on-primary-container';
+$pmy_text: 'primary-container';
+$pmy_icon: 'on-primary-container';
+$pmy_ripple: 'on-primary-container';
+
+.mfx-button.fab {
+  @include fab_base($pmy_surface, $pmy_tint, 3);
+  @include fab_disabled();
+  @include fab_hover($pmy_surface, $pmy_tint, 4);
+  @include fab_focus_press($pmy_surface, $pmy_tint, 3);
+  @include fab_icon($pmy_icon);
+  @include buttons_base.ripple($pmy_ripple);
+  @include fab_lowered(1, 2, 1);
+}
+
+/****************************************************************************************************
+ * FAB Surface
+ ****************************************************************************************************/
+$sfe_surface: 'surface';
+$sfe_tint: 'primary';
+$sfe_text: 'surface';
+$sfe_icon: 'primary';
+$sfe_ripple: 'primary';
+
+.mfx-button.fab.surface {
+  @include fab_base($sfe_surface, $sfe_tint, 3);
+  @include fab_disabled();
+  @include fab_hover($sfe_surface, $sfe_tint, 4);
+  @include fab_focus_press($sfe_surface, $sfe_tint, 3);
+  @include fab_icon($sfe_icon);
+  @include buttons_base.ripple($sfe_ripple);
+  @include fab_lowered(1, 2, 1);
+}
+
+/****************************************************************************************************
+ * FAB Secondary
+ ****************************************************************************************************/
+$sdy_surface: 'secondary-container';
+$sdy_tint: 'on-secondary-container';
+$sdy_text: 'secondary-container';
+$sdy_icon: 'on-secondary-container';
+$sdy_ripple: 'on-secondary-container';
+
+.mfx-button.fab.secondary {
+  @include fab_base($sdy_surface, $sdy_tint, 3);
+  @include fab_disabled();
+  @include fab_hover($sdy_surface, $sdy_tint, 4);
+  @include fab_focus_press($sdy_surface, $sdy_tint, 3);
+  @include fab_icon($sdy_icon);
+  @include buttons_base.ripple($sdy_ripple);
+  @include fab_lowered(1, 2, 1);
+}
+
+/****************************************************************************************************
+ * FAB Tertiary
+ ****************************************************************************************************/
+$tty_surface: 'tertiary-container';
+$tty_tint: 'on-tertiary-container';
+$tty_text: 'tertiary-container';
+$tty_icon: 'on-tertiary-container';
+$tty_ripple: 'on-tertiary-container';
+
+.mfx-button.fab.tertiary {
+  @include fab_base($tty_surface, $tty_tint, 3);
+  @include fab_disabled();
+  @include fab_hover($tty_surface, $tty_tint, 4);
+  @include fab_focus_press($tty_surface, $tty_tint, 3);
+  @include fab_icon($tty_icon);
+  @include buttons_base.ripple($tty_ripple);
+  @include fab_lowered(1, 2, 1);
+}

+ 1 - 2
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_filled_button.scss

@@ -4,14 +4,13 @@
  * Filled Buttons
  ****************************************************************************************************/
 $surface: 'primary';
-$onSurface: 'on-primary';
 $surfaceTint: 'on-primary';
 $text: 'on-primary';
 $icon: 'on-primary';
 $ripple: 'on-primary';
 
 .mfx-button.filled {
-  @include buttons_base.base($surface, $onSurface, $text, $surfaceTint, 0);
+  @include buttons_base.base($surface, $surfaceTint, $text, 0);
   @include buttons_base.disabled();
   @include buttons_base.hover($surface, $surfaceTint, 1);
   @include buttons_base.focus_press($surface, $surfaceTint, 0);

+ 1 - 2
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_outlined_button.scss

@@ -5,7 +5,6 @@
  ****************************************************************************************************/
 
 $surface: 'surface';
-$onSurface: 'primary';
 $surfaceTint: 'primary';
 $text: 'primary';
 $icon: 'primary';
@@ -13,7 +12,7 @@ $ripple: 'primary';
 
 .mfx-button.outlined {
   @include buttons_base.outline();
-  @include buttons_base.base($surface, $onSurface, $text, $surfaceTint, 0);
+  @include buttons_base.base($surface, $surfaceTint, $text, 0);
   @include buttons_base.disabled();
   @include buttons_base.hover($surface, $surfaceTint, 0);
   @include buttons_base.focus_press($surface, $surfaceTint, 0);

+ 1 - 1
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_text_button.scss

@@ -15,7 +15,7 @@ $ripple: 'primary';
   buttons_base.$padding: 0px 12px 0px 12px;
   buttons_base.$padding-i-left: 0px 16px 0px 12px;
   buttons_base.$padding-i-right: 0px 12px 0px 16px;
-  @include buttons_base.base($surface, $surface, $text, $surface, 0);
+  @include buttons_base.base($surface, $surface, $text, 0);
   @include buttons_base.disabled();
   @include buttons_base.hover(state_layer($surface, 'primary', 'hover'), $surface, 2);
   @include buttons_base.focus_press(state_layer($surface, 'primary', 'pressed'), $surface, 1);

+ 1 - 2
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_tonal_filled_button.scss

@@ -4,14 +4,13 @@
  * Tonal Filled Buttons
  ****************************************************************************************************/
 $surface: 'secondary-container';
-$onSurface: 'on-secondary-container';
 $surfaceTint: 'on-secondary-container';
 $text: 'on-secondary-container';
 $icon: 'on-secondary-container';
 $ripple: 'on-secondary-container';
 
 .mfx-button.tonal-filled {
-  @include buttons_base.base($surface, $onSurface, $text, $surfaceTint, 0);
+  @include buttons_base.base($surface, $surfaceTint, $text, 0);
   @include buttons_base.disabled();
   @include buttons_base.hover($surface, $surfaceTint, 1);
   @include buttons_base.focus_press($surface, $surfaceTint, 0);

+ 5 - 3
modules/resources/src/test/java/interactive/IconsTests.java

@@ -44,7 +44,6 @@ import org.testfx.framework.junit5.Start;
 
 import java.util.Optional;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -187,7 +186,7 @@ public class IconsTests {
 	}
 
 	@Test
-	void testWrap(FxRobot robot) throws InterruptedException, TimeoutException {
+	void testWrap(FxRobot robot) throws InterruptedException {
 		StackPane root = setupStage();
 		MFXIconWrapper wrapper = new MFXFontIcon(FontAwesomeSolid.CIRCLE.getDescription(), 64.0).wrap();
 		robot.interact(() -> root.getChildren().setAll(wrapper));
@@ -210,7 +209,6 @@ public class IconsTests {
 		));
 		Thread.sleep(sleep);
 
-		AtomicBoolean ripple = new AtomicBoolean(false);
 		robot.interact(() -> {
 			wrapper.getIcon().setDescription(FontAwesomeRegular.SQUARE.getDescription());
 			wrapper.setStyle("-mfx-enable-ripple: true;\n-mfx-round: true;\n");
@@ -225,6 +223,10 @@ public class IconsTests {
 		robot.interact(() -> wrapper.setStyle(null));
 		assertNull(wrapper.getClip());
 		assertEquals(1, wrapper.getChildren().size());
+
+		new MFXIconWrapper();
+		new MFXIconWrapper(null);
+		new MFXIconWrapper(null, 32.0);
 	}
 
 	private StackPane setupStage() {