Browse Source

:arrow_up: Upgrade components and resources modules

[Part 1] Improving and completing FABs

Behaviors Package
:recycle: MFXButtonBehavior: fire an action only on PRIMARY mouse clicks by default
:sparkles: Added specific behavior for FABs as shown by the M3 guidelines

Controls Package
:truck: Moved SkinBase from core module and renamed it to MFXSkinBase
:recycle: MFXSkinBase now doesn't keep the reference to the current behavior object, instead it is stored in the component base class (MFXControl or MFXLabeled)
:recycle: MFXControl and MFXLabeled will now automatically handle behavior creation and initialization
:recycle: For the above change, they now enforce the use of buildSkin() to create the default skin, and make createDefaultSkin() a final method, this is needed for a reliable initialization of the behavior
:recycle: MFXControl and MFXLabeled now override all the size methods to be public
:sparkles: MFXLabeled: added property that allows to control the text opacity (see recent changes to BoundLabel in core module)
:fire: MFXExtendedFab has been removed to avoid code duplication
:recycle: MFXFabBase now has a property which allows to setup the FAB as a standard one or an extended one
:recycle: MFXFabBase: since now FABs have their own specific behavior, to avoid code duplication a method has been added to retrieve the FAB behavior reference, it returns an Optional since the behavior could be null or could not be an instance of MFXFabBehavior

Skins Package
:recycle: MFXButtonSkin: bind text node opacity to the new property of MFXLabeled
:recycle: MFXButtonSkin: no need for the listener on the behaviorProvider property (good for memory and performance)
:sparkles: Implement specific skin for FABs

Theming Package
:construction: Added EXTENDED variant for FABs
:construction: Added preliminary implementation of MFXThemeManager (atm just to simplify dev)
:sparkles: Added a new class that is capable of generating CSS stylesheets via code and it's super useful. It even allows you to use multiple selectors and pseudo classes

Signed-off-by: Alessadro Parisi <alessandro.parisi406@gmail.com>
Alessadro Parisi 2 years ago
parent
commit
61976d3f82
19 changed files with 1344 additions and 233 deletions
  1. 33 0
      modules/components/CHANGELOG.md
  2. 5 2
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXButtonBehavior.java
  3. 244 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXFabBehavior.java
  4. 76 6
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXControl.java
  5. 133 8
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXLabeled.java
  6. 135 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXSkinBase.java
  7. 2 2
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXButton.java
  8. 0 107
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXExtendedFab.java
  9. 25 9
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFab.java
  10. 169 1
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFabBase.java
  11. 20 27
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXButtonSkin.java
  12. 124 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXFabSkin.java
  13. 228 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/CSSFragment.java
  14. 6 3
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/enums/FABVariants.java
  15. 53 0
      modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/enums/MFXThemeManager.java
  16. 8 0
      modules/components/src/test/java/app/ComponentsLauncher.java
  17. 27 12
      modules/components/src/test/java/app/buttons/ButtonsPlayground.java
  18. 4 4
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/components/buttons/_extended_fab.scss
  19. 52 52
      modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/mfx-light.css

+ 33 - 0
modules/components/CHANGELOG.md

@@ -16,6 +16,39 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 [//]: ##[Unreleased]
 
+## [Unreleased] - 14-03-2023
+
+## Added
+
+- Added specific behavior for FABs as shown by the M3 guidelines
+- MFXLabeled: added property that allows to control the text opacity (see recent changes to BoundLabel in core module)
+- Implement specific skin for FABs
+- Added EXTENDED variant for FABs
+- Added preliminary implementation of MFXThemeManager (atm just to simplify dev)
+- Added a new class that is capable of generating CSS stylesheets via code and it's super useful. It even allows you to
+  use multiple selectors and pseudo classes
+
+## Changed
+
+- MFXButtonBehavior: fire an action only on PRIMARY mouse clicks by default
+- Moved SkinBase from core module and renamed it to MFXSkinBase
+- MFXSkinBase now doesn't keep the reference to the current behavior object, instead it is stored in the component base
+  class (MFXControl or MFXLabeled)
+- MFXControl and MFXLabeled will now automatically handle behavior creation and initialization
+- For the above change, they now enforce the use of buildSkin() to create the default skin, and make createDefaultSkin()
+  a final method, this is needed for a reliable initialization of the behavior
+- MFXControl and MFXLabeled now override all the size methods to be public
+- MFXFabBase now has a property which allows to setup the FAB as a standard one or an extended one
+- MFXFabBase: since now FABs have their own specific behavior, to avoid code duplication a method has been added to
+  retrieve the FAB behavior reference, it returns an Optional since the behavior could be null or could not be an
+  instance of MFXFabBehavior
+- MFXButtonSkin: bind text node opacity to the new property of MFXLabeled
+- MFXButtonSkin: no need for the listener on the behaviorProvider property (good for memory and performance)
+
+## Removed
+
+- MFXExtendedFab has been removed to avoid code duplication
+
 ## [11.16.0] - 09-02-2023
 
 ## Added

+ 5 - 2
modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXButtonBehavior.java

@@ -22,6 +22,7 @@ import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton;
 import io.github.palexdev.mfxcomponents.skins.MFXButtonSkin;
 import io.github.palexdev.mfxcore.behavior.BehaviorBase;
 import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator;
+import javafx.scene.input.MouseButton;
 import javafx.scene.input.MouseEvent;
 
 /**
@@ -52,20 +53,22 @@ public class MFXButtonBehavior extends BehaviorBase<MFXButton> {
 	 * The parameters are given by the default skin, {@link MFXButtonSkin}, associated to each button.
 	 */
 	public void generateRipple(MFXRippleGenerator rg, MouseEvent me) {
+		if (me.getButton() != MouseButton.PRIMARY) return;
 		rg.generate(me);
 	}
 
 	/**
 	 * Requests focus on mouse pressed.
 	 */
-	public void mousePressed() {
+	public void mousePressed(MouseEvent me) {
 		getNode().requestFocus();
 	}
 
 	/**
 	 * Calls {@link MFXButton#fire()} on mouse clicked.
 	 */
-	public void mouseClicked() {
+	public void mouseClicked(MouseEvent me) {
+		if (me.getButton() != MouseButton.PRIMARY) return;
 		getNode().fire();
 	}
 }

+ 244 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXFabBehavior.java

@@ -0,0 +1,244 @@
+/*
+ * 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.behaviors;
+
+import io.github.palexdev.mfxcomponents.controls.base.MFXLabeled;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFabBase;
+import io.github.palexdev.mfxeffects.animations.Animations.KeyFrames;
+import io.github.palexdev.mfxeffects.animations.Animations.SequentialBuilder;
+import io.github.palexdev.mfxeffects.animations.Animations.TimelineBuilder;
+import io.github.palexdev.mfxeffects.animations.motion.M3Motion;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import javafx.animation.Animation;
+import javafx.animation.Interpolator;
+import javafx.animation.Timeline;
+import javafx.geometry.Bounds;
+import javafx.geometry.Pos;
+import javafx.scene.layout.Region;
+import javafx.scene.transform.Scale;
+import javafx.util.Duration;
+
+/**
+ * This is the default behavior used by all {@link MFXFabBase} components.
+ * <p>
+ * This behavior is an extension of {@link MFXButtonBehavior} and adds some methods to animate
+ * the FAB when needed, as described by the M3 guidelines.
+ * <p></p>
+ * Side note. Notice that if you want to animate the FAB through mouse events it is recommended to
+ * avoid triggering the ripple generator. If you don't want to disable it, then the proper way to do this
+ * is to add and event filter to the FAB and then consume the event before it can trigger the ripple generation.
+ */
+public class MFXFabBehavior extends MFXButtonBehavior {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private final Scale scale = new Scale(1, 1);
+
+	private Animation extendAnimation;
+	private boolean inhibitAnimations = false;
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXFabBehavior(MFXFabBase node) {
+		super(node);
+		node.getTransforms().add(scale);
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * This method is responsible for animating the transition between a standard FAB and
+	 * an Extended FAB as shown on the M3 guidelines website.
+	 * <p></p>
+	 * This is automatically called by the {@link MFXFabBase#extendedProperty()} when it changes.
+	 */
+	// TODO refactor this. Behavior classes should NOT change the component's state
+	public void extend(boolean animate) {
+		MFXFabBase fab = getFab();
+		boolean extended = fab.isExtended();
+		if (extended) {
+			fab.getStyleClass().remove("fab");
+			fab.getStyleClass().add("fab-extended");
+		} else {
+			fab.getStyleClass().remove("fab-extended");
+			fab.getStyleClass().add("fab");
+		}
+		double targetSize = computeWidth();
+		double targetTextOpacity = extended ? 1.0 : 0.0;
+
+		if (!animate) {
+			fab.setPrefWidth(targetSize);
+			fab.setTextOpacity(targetTextOpacity);
+			return;
+		}
+
+		double resizeDuration = inhibitAnimations ? 1 : M3Motion.LONG2.toMillis();
+		double opacityDuration = inhibitAnimations ? 1 : (extended ? M3Motion.LONG4 : M3Motion.SHORT4).toMillis();
+		Interpolator curve = M3Motion.EMPHASIZED;
+
+		if (!extended && fab.getPrefWidth() == Region.USE_COMPUTED_SIZE) fab.setPrefWidth(fab.getWidth());
+		if (extendAnimation != null) extendAnimation.stop();
+		extendAnimation = TimelineBuilder.build()
+				.add(KeyFrames.of(resizeDuration, fab.prefWidthProperty(), targetSize, curve))
+				.add(KeyFrames.of(opacityDuration, fab.textOpacityProperty(), targetTextOpacity, curve))
+				.getAnimation();
+		extendAnimation.play();
+
+	}
+
+	/**
+	 * Calls {@link #changeIcon(MFXFontIcon, Pos)} with {@link Pos#BOTTOM_RIGHT} as the pivot.
+	 */
+	public void changeIcon(MFXFontIcon newIcon) {
+		changeIcon(newIcon, Pos.BOTTOM_RIGHT);
+	}
+
+	/**
+	 * This is responsible for animating the FAB when the icon changes.
+	 * For a smooth transition, instead of using {@link MFXFabBase#setIcon(MFXFontIcon)}, one should
+	 * use this instead, thus triggering the animation.
+	 * <p></p>
+	 * The M3 guidelines show a scale transition for regular FABs and a quick collapse/extend transition for
+	 * Extended variants.
+	 * <p></p>
+	 * The 'pivot' argument is used to set the {@link Scale}'s pivotX and pivotY, the code is factored out in the
+	 * {@link #setScalePivot(Pos)} method.
+	 */
+	public void changeIcon(MFXFontIcon newIcon, Pos pivot) {
+		MFXFabBase fab = getFab();
+		MFXFontIcon currentIcon = fab.getIcon();
+
+		Duration outMillis = M3Motion.LONG4;
+		if (fab.isExtended()) {
+			newIcon.setOpacity(0.0);
+			fab.setIcon(newIcon);
+			TimelineBuilder.build()
+					.add(KeyFrames.of(0, e -> {
+						inhibitAnimations = true;
+						fab.setExtended(false);
+					}))
+					.add(KeyFrames.of(M3Motion.SHORT2, e -> {
+						inhibitAnimations = false;
+						fab.setExtended(true);
+					}))
+					.add(KeyFrames.of(outMillis, newIcon.opacityProperty(), 1.0, M3Motion.EMPHASIZED))
+					.getAnimation()
+					.play();
+		} else {
+			setScalePivot(pivot);
+
+			Duration inMillis = M3Motion.SHORT4;
+			Interpolator inCurve = M3Motion.EMPHASIZED_ACCELERATE;
+			Interpolator outCurve = M3Motion.EMPHASIZED_DECELERATE;
+
+			Timeline scaleDown = TimelineBuilder.build()
+					.addConditional(() -> currentIcon != null, KeyFrames.of(inMillis, currentIcon.opacityProperty(), 0.0, inCurve))
+					.add(KeyFrames.of(inMillis, scale.xProperty(), 0.0, inCurve))
+					.add(KeyFrames.of(inMillis, scale.yProperty(), 0.0, inCurve))
+					.add(KeyFrames.of(inMillis, e -> fab.setIcon(newIcon)))
+					.getAnimation();
+			Timeline scaleUp = TimelineBuilder.build()
+					.addConditional(() -> newIcon != null, KeyFrames.of(0, e -> newIcon.setOpacity(0.0)))
+					.add(KeyFrames.of(outMillis, scale.xProperty(), 1.0, outCurve))
+					.add(KeyFrames.of(outMillis, scale.yProperty(), 1.0, outCurve))
+					.addConditional(() -> newIcon != null, KeyFrames.of(outMillis, newIcon.opacityProperty(), 1.0, outCurve))
+					.getAnimation();
+			SequentialBuilder.build()
+					.add(scaleDown)
+					.add(scaleUp)
+					.getAnimation()
+					.play();
+		}
+	}
+
+	/**
+	 * Responsible for computing the target width when collapsing/extending the FAB,
+	 * a simple delegate for {@link MFXLabeled#computePrefWidth(double)}.
+	 */
+	@SuppressWarnings("JavadocReference")
+	protected double computeWidth() {
+		MFXFabBase fab = getFab();
+		return fab.computePrefWidth(-1);
+	}
+
+	/**
+	 * This is responsible for setting the {@link Scale}'s pivotX and pivotY for animating the
+	 * FAB when changing icon through {@link #changeIcon(MFXFontIcon)}.
+	 * <p></p>
+	 * Supported values are: {@code TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER_LEFT, CENTER_RIGHT}.
+	 */
+	protected void setScalePivot(Pos pos) {
+		MFXFabBase fab = getFab();
+		switch (pos) {
+			case TOP_LEFT: {
+				scale.pivotXProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMinX));
+				scale.pivotYProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMinY));
+				break;
+			}
+			case TOP_RIGHT: {
+				scale.pivotXProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMaxX));
+				scale.pivotYProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMinY));
+				break;
+			}
+			case BOTTOM_LEFT: {
+				scale.pivotXProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMinX));
+				scale.pivotYProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMaxY));
+				break;
+			}
+			case CENTER_LEFT: {
+				scale.pivotXProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMinX));
+				scale.pivotYProperty().bind(fab.layoutBoundsProperty().map(b -> b.getMaxY() / 2.0));
+				break;
+			}
+			case CENTER_RIGHT: {
+				scale.pivotXProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMaxX));
+				scale.pivotYProperty().bind(fab.layoutBoundsProperty().map(b -> b.getMaxY() / 2.0));
+				break;
+			}
+			default:
+			case BOTTOM_RIGHT: {
+				scale.pivotXProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMaxX));
+				scale.pivotYProperty().bind(fab.layoutBoundsProperty().map(Bounds::getMaxY));
+			}
+		}
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+	@Override
+	public void dispose() {
+		getNode().getTransforms().remove(scale);
+		super.dispose();
+	}
+
+	//================================================================================
+	// Getters
+	//================================================================================
+
+	/**
+	 * @return {@link #getNode()} cast to {@link MFXFabBase}
+	 */
+	public MFXFabBase getFab() {
+		return (MFXFabBase) getNode();
+	}
+}

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

@@ -18,11 +18,11 @@
 
 package io.github.palexdev.mfxcomponents.controls.base;
 
+import io.github.palexdev.mfxcomponents.behaviors.MFXFabBehavior;
 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;
@@ -38,6 +38,22 @@ import java.util.function.Supplier;
  * that perfectly integrates with the new Behavior and Theming APIs.
  * <p>
  * Extends {@link Control} and implements both {@link WithBehavior} and {@link MFXStyleable}.
+ * Enforces the use of {@link MFXSkinBase} instances as Skin implementations and makes the {@link #createDefaultSkin()}
+ * final thus denying users to override it. To set custom skins you should override the new provided method {@link #buildSkin()}.
+ * <p>
+ * I wanted to avoid adding a listener of the skin property for memory and performance reasons. Every time a skin is created
+ * it's needed to pass the current built behavior to the skin for initialization. A good hook place for this call was the
+ * {@link #createDefaultSkin()} method, but this would make it harder for users to override it because then you would also
+ * have to take into account the behavior initialization. Having a new method maintains the usual simplicity of setting
+ * custom skins while avoiding listeners for better performance.
+ * <p></p>
+ * The integration with the new Behavior API is achieved by having a specific property, {@link #behaviorProviderProperty()},
+ * which allows to change at any time the component's behavior. The property automatically handles initialization and disposal
+ * of behaviors. A reference to th current built behavior object is kept to be retrieved via {@link #getBehavior()}.
+ * <p>
+ * In MaterialFX, the Behavior API is not a closed API, it's not meant to be private. A user can always take it and invoke
+ * its methods directly, extend it, suppress it, do whatever you like. Also, some components' behavior may specify methods
+ * that are meant to be called by the user when needed, see {@link MFXFabBehavior} as an example.
  * <p></p>
  * Components that primarily deal with text should extend {@link MFXLabeled} instead.
  * <p></p>
@@ -46,11 +62,28 @@ import java.util.function.Supplier;
  *
  * @param <B> the behavior type used by the control
  */
+@SuppressWarnings({"unchecked", "rawtypes"})
 public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends Control implements WithBehavior<B>, MFXStyleable {
 	//================================================================================
 	// Properties
 	//================================================================================
-	private final SupplierProperty<B> behaviorProvider = new SupplierProperty<>();
+	private B behavior;
+	private final SupplierProperty<B> behaviorProvider = new SupplierProperty<>() {
+		@Override
+		protected void invalidated() {
+			if (behavior != null) {
+				behavior.dispose();
+			}
+			behavior = get().get();
+			MFXSkinBase skin = (MFXSkinBase) getSkin();
+			if (skin != null && behavior != null) skin.initBehavior(behavior);
+		}
+	};
+
+	//================================================================================
+	// Abstract Methods
+	//================================================================================
+	protected abstract MFXSkinBase<?, ?> buildSkin();
 
 	//================================================================================
 	// Methods
@@ -84,8 +117,40 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	// Overridden Methods
 	//================================================================================
 	@Override
-	protected SkinBase<?, ?> createDefaultSkin() {
-		return null;
+	public double computeMinWidth(double height) {
+		return super.computeMinWidth(height);
+	}
+
+	@Override
+	public double computeMinHeight(double width) {
+		return super.computeMinHeight(width);
+	}
+
+	@Override
+	public double computePrefWidth(double height) {
+		return super.computePrefWidth(height);
+	}
+
+	@Override
+	public double computePrefHeight(double width) {
+		return super.computePrefHeight(width);
+	}
+
+	@Override
+	public double computeMaxWidth(double height) {
+		return super.computeMaxWidth(height);
+	}
+
+	@Override
+	public double computeMaxHeight(double width) {
+		return super.computeMaxHeight(width);
+	}
+
+	@Override
+	protected final MFXSkinBase<?, ?> createDefaultSkin() {
+		MFXSkinBase skin = buildSkin();
+		if (behavior != null) skin.initBehavior(behavior);
+		return skin;
 	}
 
 	//================================================================================
@@ -117,7 +182,7 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 		}
 	};
 
-	protected final double getInitHeight() {
+	public final double getInitHeight() {
 		return initHeight.get();
 	}
 
@@ -142,7 +207,7 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 		this.initHeight.set(initHeight);
 	}
 
-	protected final double getInitWidth() {
+	public final double getInitWidth() {
 		return initWidth.get();
 	}
 
@@ -208,6 +273,11 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 	// Getters/Setters
 	//================================================================================
+	@Override
+	public B getBehavior() {
+		return behavior;
+	}
+
 	@Override
 	public Supplier<B> getBehaviorProvider() {
 		return behaviorProvider.get();

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

@@ -18,11 +18,11 @@
 
 package io.github.palexdev.mfxcomponents.controls.base;
 
+import io.github.palexdev.mfxcomponents.behaviors.MFXFabBehavior;
 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;
@@ -35,20 +35,49 @@ import java.util.function.Supplier;
 
 /**
  * Base class for MaterialFX controls that are text based. The idea is to have a separate hierarchy of components from the JavaFX one,
- * * that perfectly integrates with the new Behavior and Theming APIs.
+ * that perfectly integrates with the new Behavior, Skin and Theming APIs.
  * <p>
  * Extends {@link Labeled} and implements both {@link WithBehavior} and {@link MFXStyleable}.
+ * Enforces the use of {@link MFXSkinBase} instances as Skin implementations and makes the {@link #createDefaultSkin()}
+ * final thus denying users to override it. To set custom skins you should override the new provided method {@link #buildSkin()}.
+ * <p>
+ * I wanted to avoid adding a listener of the skin property for memory and performance reasons. Every time a skin is created
+ * it's needed to pass the current built behavior to the skin for initialization. A good hook place for this call was the
+ * {@link #createDefaultSkin()} method, but this would make it harder for users to override it because then you would also
+ * have to take into account the behavior initialization. Having a new method maintains the usual simplicity of setting
+ * custom skins while avoiding listeners for better performance.
+ * <p></p>
+ * The integration with the new Behavior API is achieved by having a specific property, {@link #behaviorProviderProperty()},
+ * which allows to change at any time the component's behavior. The property automatically handles initialization and disposal
+ * of behaviors. A reference to th current built behavior object is kept to be retrieved via {@link #getBehavior()}.
+ * <p>
+ * In MaterialFX, the Behavior API is not a closed API, it's not meant to be private. A user can always take it and invoke
+ * its methods directly, extend it, suppress it, do whatever you like. Also, some components' behavior may specify methods
+ * that are meant to be called by the user when needed, see {@link MFXFabBehavior} as an example.
  * <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
+ * @see MFXSkinBase
  */
+@SuppressWarnings({"unchecked", "rawtypes"})
 public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends Labeled implements WithBehavior<B>, MFXStyleable {
 	//================================================================================
 	// Properties
 	//================================================================================
-	private final SupplierProperty<B> behaviorProvider = new SupplierProperty<>();
+	private B behavior;
+	private final SupplierProperty<B> behaviorProvider = new SupplierProperty<>() {
+		@Override
+		protected void invalidated() {
+			if (behavior != null) {
+				behavior.dispose();
+			}
+			behavior = get().get();
+			MFXSkinBase skin = ((MFXSkinBase) getSkin());
+			if (skin != null && behavior != null) skin.initBehavior(behavior);
+		}
+	};
 
 	//================================================================================
 	// Constructors
@@ -64,6 +93,15 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		super(text, graphic);
 	}
 
+	//================================================================================
+	// Abstract Methods
+	//================================================================================
+
+	/**
+	 * Create a new instance of the default skin for this component.
+	 */
+	protected abstract MFXSkinBase<?, ?> buildSkin();
+
 	//================================================================================
 	// Methods
 	//================================================================================
@@ -96,13 +134,63 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	// Overridden Methods
 	//================================================================================
 	@Override
-	protected SkinBase<?, ?> createDefaultSkin() {
-		return null;
+	public double computeMinWidth(double height) {
+		return super.computeMinWidth(height);
+	}
+
+	@Override
+	public double computeMinHeight(double width) {
+		return super.computeMinHeight(width);
+	}
+
+	@Override
+	public double computePrefWidth(double height) {
+		return super.computePrefWidth(height);
+	}
+
+	@Override
+	public double computePrefHeight(double width) {
+		return super.computePrefHeight(width);
+	}
+
+	@Override
+	public double computeMaxWidth(double height) {
+		return super.computeMaxWidth(height);
+	}
+
+	@Override
+	public double computeMaxHeight(double width) {
+		return super.computeMaxHeight(width);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p></p>
+	 * Overridden to make sure the behavior object is initialized by the skin upon its creation.
+	 *
+	 * @see MFXSkinBase
+	 */
+	@Override
+	protected final MFXSkinBase<?, ?> createDefaultSkin() {
+		MFXSkinBase skin = buildSkin();
+		if (behavior != null) skin.initBehavior(behavior);
+		return skin;
 	}
 
 	//================================================================================
 	// Styleable Properties
 	//================================================================================
+	// TODO should this be handled by the skin? (could also be handled in the overridden methods above though)
+	// TODO if so, should we add a switch to override this behavior so that a user doesn't have to create a custom skin?
+	// TODO One idea is to have 'Layout Strategies'. The problem is, let's suppose component A overrides the computePrefWidth method
+	// TODO and then a component B want to override it again, but it needs to original computation, the one produced by the superclass of A
+	// TODO there would be no way to regain the old computation unless copy-paste of code.
+	// TODO With 'Layout Strategies' components could define functions for each of the computation methods (min, pref and max sizes),
+	// TODO avoiding at least code duplication
+	//
+	// TODO these are officially DEPRECATED as for some reason they cause a huge performance overhead
+	// TODO 'Layout Strategies' may be a good alternative at this point, although values would be hard coded, still pretty
+	// TODO easy to replace though
 	private final StyleableDoubleProperty initHeight = new StyleableDoubleProperty(
 			StyleableProperties.INIT_HEIGHT,
 			this,
@@ -129,7 +217,14 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		}
 	};
 
-	protected final double getInitHeight() {
+	private final StyleableDoubleProperty textOpacity = new StyleableDoubleProperty(
+			StyleableProperties.TEXT_OPACITY,
+			this,
+			"textOpacity",
+			1.0
+	);
+
+	public final double getInitHeight() {
 		return initHeight.get();
 	}
 
@@ -154,7 +249,7 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		this.initHeight.set(initHeight);
 	}
 
-	protected final double getInitWidth() {
+	public final double getInitWidth() {
 		return initWidth.get();
 	}
 
@@ -179,6 +274,24 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		this.initWidth.set(initWidth);
 	}
 
+	public double getTextOpacity() {
+		return textOpacity.get();
+	}
+
+	/**
+	 * It is now possible through this property to specify the opacity of the text node of components
+	 * extending this.
+	 * <p></p>
+	 * Can be set in CSS via the property: '-mfx-text-opacity'.
+	 */
+	public StyleableDoubleProperty textOpacityProperty() {
+		return textOpacity;
+	}
+
+	public void setTextOpacity(double textOpacity) {
+		this.textOpacity.set(textOpacity);
+	}
+
 	//================================================================================
 	// CssMetaData
 	//================================================================================
@@ -200,10 +313,17 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 						USE_COMPUTED_SIZE
 				);
 
+		private static final CssMetaData<MFXLabeled<?>, Number> TEXT_OPACITY =
+				FACTORY.createSizeCssMetaData(
+						"-mfx-text-opacity",
+						MFXLabeled::textOpacityProperty,
+						1.0
+				);
+
 		static {
 			cssMetaDataList = StyleUtils.cssMetaDataList(
 					Labeled.getClassCssMetaData(),
-					INIT_HEIGHT, INIT_WIDTH
+					INIT_HEIGHT, INIT_WIDTH, TEXT_OPACITY
 			);
 		}
 	}
@@ -220,6 +340,11 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 	// Getters/Setters
 	//================================================================================
+	@Override
+	public B getBehavior() {
+		return behavior;
+	}
+
 	@Override
 	public Supplier<B> getBehaviorProvider() {
 		return behaviorProvider.get();

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

@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 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.base;
+
+import io.github.palexdev.mfxcore.behavior.BehaviorBase;
+import io.github.palexdev.mfxcore.behavior.WithBehavior;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.SkinBase;
+
+import java.util.Optional;
+
+/**
+ * Extension of {@link SkinBase} used by components that what a seamless integration with the new Behavior API.
+ * <p>
+ * The skin in responsible for initializing the behavior as needed. The new model of MaterialFX components has now three
+ * main parts:
+ * <p> - the Control, which is the component, the class has all its specs
+ * <p> - the View, defines the component's look/layout
+ * <p> - the Behavior, defines what the component can do and how
+ * <p>
+ * So, as you may guess, there must be an 'infrastructure' that makes all these three parts communicate with each other.
+ * The behavior may need to be connected with the specs of the component, as well as with its subcomponents defined in
+ * its view.
+ * <p>
+ * {@link MFXControl} and {@link MFXLabeled} are a bridge between these three parts. They retain the reference of the current
+ * built behavior object, which can be retrieved via {@link WithBehavior#getBehavior()}. This way skins that need to register
+ * handlers or listeners that will then depend on the behavior can do it very easily. The two aforementioned base classes
+ * are responsible for calling {@link #initBehavior(BehaviorBase)} every time the behavior changes, as well as dispose it of course.
+ * <p>
+ * The development flow for controls with the new Behavior and Skin API would be:
+ * <p> - Have a components that extends either {@link MFXControl}, {@link MFXLabeled} or any of their subclasses
+ * <p> - Having an implementation of this base Skin, either one of the already provided or a custom one
+ * <p> - Having a behavior class and set the provider on the component
+ * <p> - Override the {@link #initBehavior(BehaviorBase)} to initialize the behavior if needed
+ * <p> - Initialization and changes to the behavior provider are automatically handled, hassle-free
+ */
+public abstract class MFXSkinBase<C extends Control & WithBehavior<B>, B extends BehaviorBase<C>> extends javafx.scene.control.SkinBase<C> {
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	protected MFXSkinBase(C control) {
+		super(control);
+	}
+
+	//================================================================================
+	// Abstract Methods
+	//================================================================================
+
+	/**
+	 * This is responsible for initializing the behavior every time it changes, the given parameter
+	 * is the current uninitialized behavior.
+	 */
+	protected abstract void initBehavior(B behavior);
+
+	//================================================================================
+	// Delegate Methods
+	//================================================================================
+
+	// TODO behaviors should also integrate with the new When constructs if possible
+
+	/**
+	 * Delegate for {@link BehaviorBase#register(ObservableValue, ChangeListener)}.
+	 * <p>
+	 * Note this will do nothing if the return value of {@link #getBehavior()} is 'null'.
+	 */
+	public <T> void register(ObservableValue<T> observable, ChangeListener<T> listener) {
+		Optional.ofNullable(getBehavior()).ifPresent(b -> b.register(observable, listener));
+	}
+
+	/**
+	 * Delegate for {@link BehaviorBase#register(Observable, InvalidationListener)}.
+	 * <p>
+	 * Note this will do nothing if the return value of {@link #getBehavior()} is 'null'.
+	 */
+	public void register(Observable observable, InvalidationListener listener) {
+		Optional.ofNullable(getBehavior()).ifPresent(b -> b.register(observable, listener));
+	}
+
+	/**
+	 * Delegate for {@link BehaviorBase#handler(Node, EventType, EventHandler)}.
+	 * <p>
+	 * Note this will do nothing if the return value of {@link #getBehavior()} is 'null'.
+	 */
+	public <E extends Event> void handle(Node node, EventType<E> eventType, EventHandler<E> handler) {
+		Optional.ofNullable(getBehavior()).ifPresent(b -> b.handler(node, eventType, handler));
+	}
+
+	/**
+	 * Delegate for {@link BehaviorBase#filter(Node, EventType, EventHandler)}.
+	 * <p>
+	 * Note this will do nothing if the return value of {@link #getBehavior()} is 'null'.
+	 */
+	public <E extends Event> void handleAsFilter(Node node, EventType<E> eventType, EventHandler<E> handler) {
+		Optional.ofNullable(getBehavior()).ifPresent(b -> b.filter(node, eventType, handler));
+	}
+
+	//================================================================================
+	// Getters/Setters
+	//================================================================================
+
+	/**
+	 * Delegate for {@link WithBehavior#getBehavior()}.
+	 * <p>
+	 * Since this is called on the component, the return value could also be null if the behavior
+	 * provider was not set, or produces null references.
+	 */
+	protected B getBehavior() {
+		return getSkinnable().getBehavior();
+	}
+}

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

@@ -20,9 +20,9 @@ package io.github.palexdev.mfxcomponents.controls.buttons;
 
 import io.github.palexdev.mfxcomponents.behaviors.MFXButtonBehavior;
 import io.github.palexdev.mfxcomponents.controls.base.MFXLabeled;
+import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
 import io.github.palexdev.mfxcomponents.skins.MFXButtonSkin;
 import io.github.palexdev.mfxcore.base.properties.EventHandlerProperty;
-import io.github.palexdev.mfxcore.controls.SkinBase;
 import io.github.palexdev.mfxcore.observables.When;
 import io.github.palexdev.mfxcore.utils.fx.SceneBuilderIntegration;
 import io.github.palexdev.mfxresources.MFXResources;
@@ -147,7 +147,7 @@ public class MFXButton extends MFXLabeled<MFXButtonBehavior> {
 	}
 
 	@Override
-	protected SkinBase<?, ?> createDefaultSkin() {
+	protected MFXSkinBase<?, ?> buildSkin() {
 		return new MFXButtonSkin(this);
 	}
 

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

@@ -1,107 +0,0 @@
-/*
- * 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;
-	}
-}
-

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

@@ -40,12 +40,19 @@ public class MFXFab extends MFXFabBase implements WithVariants<MFXFab, FABVarian
 	// Constructors
 	//================================================================================
 	public MFXFab() {
-		this(null);
+		this("");
+	}
+
+	public MFXFab(String text) {
+		this(text, null);
 	}
 
 	public MFXFab(MFXFontIcon icon) {
-		super("", icon);
-		initialize();
+		this("", icon);
+	}
+
+	public MFXFab(String text, MFXFontIcon icon) {
+		super(text, icon);
 	}
 
 	/**
@@ -83,11 +90,20 @@ public class MFXFab extends MFXFabBase implements WithVariants<MFXFab, FABVarian
 		return new MFXFab().setVariants(FABVariants.TERTIARY);
 	}
 
-	//================================================================================
-	// Methods
-	//================================================================================
-	private void initialize() {
-		textProperty().addListener(i -> setText(""));
+	/**
+	 * Creates a new {@code MFXFab} with less shadow emphasis.
+	 */
+	public static MFXFab lowered() {
+		return new MFXFab().setVariants(FABVariants.LOWERED);
+	}
+
+	/**
+	 * Creates a new {@link MFXFab} which is extended (shows text).
+	 */
+	public static MFXFab extended() {
+		MFXFab fab = new MFXFab();
+		fab.setExtended(true);
+		return fab;
 	}
 
 	//================================================================================
@@ -95,7 +111,7 @@ public class MFXFab extends MFXFabBase implements WithVariants<MFXFab, FABVarian
 	//================================================================================
 	@Override
 	public List<String> defaultStyleClasses() {
-		return List.of("mfx-button", "fab");
+		return List.of("mfx-button", "fab-base", "fab");
 	}
 
 	@Override

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

@@ -18,18 +18,39 @@
 
 package io.github.palexdev.mfxcomponents.controls.fab;
 
+import io.github.palexdev.mfxcomponents.behaviors.MFXButtonBehavior;
+import io.github.palexdev.mfxcomponents.behaviors.MFXFabBehavior;
+import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
 import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton;
 import io.github.palexdev.mfxcomponents.controls.buttons.MFXElevatedButton;
+import io.github.palexdev.mfxcomponents.skins.MFXFabSkin;
+import io.github.palexdev.mfxcore.base.properties.styleable.StyleableBooleanProperty;
+import io.github.palexdev.mfxcore.observables.When;
+import io.github.palexdev.mfxcore.utils.fx.StyleUtils;
 import io.github.palexdev.mfxresources.base.properties.IconProperty;
 import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import javafx.css.CssMetaData;
+import javafx.css.Styleable;
+import javafx.css.StyleablePropertyFactory;
+import javafx.scene.control.Skin;
 
 import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
 
 /**
  * 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}.
+ * This base class has one variant: {@link MFXFab}.
+ * M3 guidelines also show the Extended variant. Since they also show that a standard FAB can transition to an Extended
+ * one through an animation, I decided to merge the Extended variant in the standard one, add a property to extend it,
+ * {@link #extendedProperty()}, and implement FAB specific behavior for animations.
+ * <p>
+ * The default behavior for all {@link MFXFabBase} components is {@link MFXFabBehavior}. Since this extends {@link MFXElevatedButton} to
+ * avoid code duplication, it expects behaviors of type {@link MFXButtonBehavior} which is correct since FABs are particular types
+ * of buttons. To also avoid messing up with Java generics and inheritance (never ends well), I added a new method
+ * to retrieve the FAB's behavior instance, {@link #getFabBehavior()}.
  * <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.
@@ -72,19 +93,166 @@ public class MFXFabBase extends MFXElevatedButton {
 	private void initialize() {
 		graphicProperty().bind(icon);
 		sceneBuilderIntegration();
+
+		// This is needed since the default value is 'false'
+		// This makes the FAB have the correct sizes at init when "collapsed"
+		extend();
+	}
+
+	/**
+	 * This is responsible for triggering the animation that changes the FAB to an Extended one or vice-versa when the
+	 * {@link #extendedProperty()} changes.
+	 * <p></p>
+	 * Implementation details. The default state is standard (non-extended). For this reason this needs to be executed as
+	 * soon as the component is created so that it has the correct sizes. When called upon initialization the skin will be
+	 * most certainly null, meaning that the animation will need to be postponed.
+	 * <p>
+	 * Also note that this uses {@link #getFabBehavior()} to get the behavior, since it's 'null' safe and exception safe.
+	 */
+	protected final void extend() {
+		Skin<?> skin = getSkin();
+		if (skin == null) {
+			// Let's ensure that no other listeners have been added before...
+			When.disposeFor(skinProperty());
+
+			// This is needed because if this property is set before the Skin has been
+			// created it's not possible for the control to correctly compute its
+			// expanded/collapsed size. So, first of all we must wait until the Skin is created,
+			// and then we must also make sure that the control is in the right 'layout state'.
+			// What I mean is that even if the Skin is created there's no guarantee that the
+			// sizes will be correct, remember JavaFX is hot garbage. For this reason we force
+			// to apply the CSS and compute the layout, this should ensure the correctness of the
+			// measurements.
+			When.onChanged(skinProperty())
+					.condition((o, n) -> n != null)
+					.then((o, n) -> {
+						applyCss();
+						layout();
+						getFabBehavior().ifPresent(b -> b.extend(false));
+					})
+					.oneShot()
+					.listen();
+			return;
+		}
+		getFabBehavior().ifPresent(b -> b.extend(true));
 	}
 
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
+	@Override
+	protected void applyInitSizes(boolean force) {
+		super.applyInitSizes(force);
+
+		// This usually happens when the FAB changes between standard and extended
+		// In such cases it's important to ensure minimum sizes are correct
+		getFabBehavior().ifPresent(b -> b.extend(false));
+	}
+
+	@Override
+	public Supplier<MFXButtonBehavior> defaultBehaviorProvider() {
+		return () -> new MFXFabBehavior(this);
+	}
+
 	@Override
 	public List<String> defaultStyleClasses() {
 		return List.of("mfx-button", "fab-base");
 	}
 
+	@Override
+	public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+		return getClassCssMetaData();
+	}
+
+	@Override
+	protected MFXSkinBase<?, ?> buildSkin() {
+		return new MFXFabSkin(this);
+	}
+
+	//================================================================================
+	// Styleable Properties
+	//================================================================================
+	private final StyleableBooleanProperty extended = new StyleableBooleanProperty(
+			StyleableProperties.EXTENDED,
+			this,
+			"extended",
+			false
+	) {
+		@Override
+		protected void invalidated() {
+			extend();
+		}
+	};
+
+	public boolean isExtended() {
+		return extended.get();
+	}
+
+	/**
+	 * Specifies whether the FAB also shows its text or not.
+	 * <p>
+	 * By default, the change of this property will trigger the animated transition defined in {@link MFXFabBehavior},
+	 * can be avoided by changing the behavior, overriding the behavior method, or simply by overriding {@link #extend()}
+	 * <p></p>
+	 * Also note that {@link MFXFabBehavior#extend(boolean)} is responsible for activating the ":extended" pseudo class on the
+	 * FAB which may change the component's appearance if specified by the current active theme.
+	 * <p></p>
+	 * Can be set in CSS via the property: '-mfx-extended'.
+	 */
+	public StyleableBooleanProperty extendedProperty() {
+		return extended;
+	}
+
+	public void setExtended(boolean extended) {
+		this.extended.set(extended);
+	}
+
+	//================================================================================
+	// CssMetaData
+	//================================================================================
+	private static class StyleableProperties {
+		private static final StyleablePropertyFactory<MFXFabBase> FACTORY = new StyleablePropertyFactory<>(MFXElevatedButton.getClassCssMetaData());
+		private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+		private static final CssMetaData<MFXFabBase, Boolean> EXTENDED =
+				FACTORY.createBooleanCssMetaData(
+						"-mfx-extended",
+						MFXFabBase::extendedProperty,
+						false
+				);
+
+		static {
+			cssMetaDataList = StyleUtils.cssMetaDataList(
+					MFXElevatedButton.getClassCssMetaData(),
+					EXTENDED
+			);
+		}
+	}
+
+	public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+		return StyleableProperties.cssMetaDataList;
+	}
+
 	//================================================================================
 	// Getters/Setters
 	//================================================================================
+
+	/**
+	 * Since this extends {@link MFXElevatedButton} to avoid code duplication, the inherited {@link #getBehavior()} method
+	 * expects to return an instance of {@link MFXButtonBehavior}. To also avoid messing up with Java generics and inheritance
+	 * (never ends well), and since the FAB is intended to be used with instances of {@link MFXFabBehavior} as the behavior
+	 * anyway I added this method that is both 'null' safe and exception safe, in fact it returns an {@link Optional}
+	 * that may or may not encapsulate the current FAB behavior instance (this means that if the behavior is not an instance
+	 * of {@code MFXFabBehavior} it will return an empty {@code Optional}).
+	 */
+	public Optional<MFXFabBehavior> getFabBehavior() {
+		try {
+			return Optional.of(((MFXFabBehavior) getBehavior()));
+		} catch (Exception ex) {
+			return Optional.empty();
+		}
+	}
+
 	public MFXFontIcon getIcon() {
 		return iconProperty().get();
 	}

+ 20 - 27
modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXButtonSkin.java

@@ -19,11 +19,10 @@
 package io.github.palexdev.mfxcomponents.skins;
 
 import io.github.palexdev.mfxcomponents.behaviors.MFXButtonBehavior;
+import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
 import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton;
-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;
 import io.github.palexdev.mfxcore.utils.fx.TextUtils;
 import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator;
@@ -38,17 +37,17 @@ import static io.github.palexdev.mfxcore.observables.When.onChanged;
 /**
  * Default skin implementation for {@link MFXButton}s.
  * <p>
- * Extends {@link SkinBase} allowing seamless integration with the new Behavior API. This
+ * Extends {@link MFXSkinBase} allowing seamless integration with the new Behavior API. This
  * skin uses behaviors of type {@link MFXButtonBehavior}.
  * <p></p>
  * The layout is simple, there are just the label to show the text and the {@link MFXRippleGenerator} to generate ripples.
  */
-public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
+public class MFXButtonSkin extends MFXSkinBase<MFXButton, MFXButtonBehavior> {
 	//================================================================================
 	// Properties
 	//================================================================================
-	private final BoundLabel label;
-	private final MFXRippleGenerator rg;
+	protected final BoundLabel label;
+	protected final MFXRippleGenerator rg;
 
 	//================================================================================
 	// Constructors
@@ -58,6 +57,8 @@ public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
 
 		// Init nodes
 		label = new BoundLabel(button);
+		label.onSetTextNode(n -> n.opacityProperty().bind(button.textOpacityProperty()));
+
 		rg = new MFXRippleGenerator(button);
 		rg.setManaged(false);
 		rg.setAnimateBackground(false);
@@ -76,30 +77,22 @@ 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#contentDisplayProperty()} to activate/disable the pseudo classes
 	 * {@link PseudoClasses#WITH_ICON_LEFT} and {@link PseudoClasses#WITH_ICON_RIGHT} accordingly
 	 */
-	private void addListeners() {
+	protected void addListeners() {
 		MFXButton button = getSkinnable();
-		onChanged(button.behaviorProviderProperty())
-				.then((o, n) -> setBehavior(n.get()))
+		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();
-		}
 	}
 
 	//================================================================================
@@ -114,8 +107,8 @@ public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
 	protected void initBehavior(MFXButtonBehavior behavior) {
 		MFXButton button = getSkinnable();
 		handle(button, MouseEvent.MOUSE_PRESSED, e -> behavior.generateRipple(rg, e));
-		handle(button, MouseEvent.MOUSE_PRESSED, e -> behavior.mousePressed());
-		handle(button, MouseEvent.MOUSE_CLICKED, e -> behavior.mouseClicked());
+		handle(button, MouseEvent.MOUSE_PRESSED, behavior::mousePressed);
+		handle(button, MouseEvent.MOUSE_CLICKED, behavior::mouseClicked);
 	}
 
 	@Override
@@ -158,7 +151,7 @@ public class MFXButtonSkin extends SkinBase<MFXButton, MFXButtonBehavior> {
 	@Override
 	public void dispose() {
 		MFXButton button = getSkinnable();
-		disposeFor(button.behaviorProviderProperty());
+		label.getTextNode().ifPresent(n -> n.opacityProperty().unbind());
 		disposeFor(button.contentDisplayProperty());
 		super.dispose();
 	}

+ 124 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXFabSkin.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 io.github.palexdev.mfxcomponents.skins;
+
+import io.github.palexdev.mfxcomponents.behaviors.MFXFabBehavior;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFabBase;
+import io.github.palexdev.mfxcomponents.theming.enums.PseudoClasses;
+import io.github.palexdev.mfxcore.base.beans.Position;
+import io.github.palexdev.mfxcore.utils.fx.LayoutUtils;
+import io.github.palexdev.mfxcore.utils.fx.TextUtils;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.VPos;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.layout.Region;
+
+import static io.github.palexdev.mfxcore.observables.When.onChanged;
+
+/**
+ * Default skin implementation for {@link MFXFabBase} components.
+ * <p>
+ * Extends {@link MFXButtonSkin}.
+ * <p></p>
+ * This is needed for the animations defined in {@link MFXFabBehavior} to work properly.
+ * <p>
+ * The min width computation is overridden to return {@link Region#USE_COMPUTED_SIZE}, while the
+ * pref width property is overridden to adapt to the {@link MFXFabBase#extendedProperty()}.
+ * <p></p>
+ * The layout strategy is also overridden so that the label is never truncated, this is also needed for the animations
+ * to look as expected. Also, FABs are not supposed to be truncated since they are important UI elements.
+ */
+public class MFXFabSkin extends MFXButtonSkin {
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public MFXFabSkin(MFXFabBase fab) {
+		super(fab);
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+	@Override
+	protected void addListeners() {
+		MFXFabBase fab = getFab();
+		onChanged(fab.contentDisplayProperty())
+				.then((o, n) -> {
+					if (!fab.isExtended()) {
+						PseudoClasses.WITH_ICON_LEFT.setOn(fab, false);
+						PseudoClasses.WITH_ICON_RIGHT.setOn(fab, false);
+						return;
+					}
+					MFXFontIcon icon = fab.getIcon();
+					boolean wil = (icon != null) && (n == ContentDisplay.LEFT);
+					boolean wir = (icon != null) && (n == ContentDisplay.RIGHT);
+					PseudoClasses.WITH_ICON_LEFT.setOn(fab, wil);
+					PseudoClasses.WITH_ICON_RIGHT.setOn(fab, wir);
+				})
+				.executeNow()
+				.invalidating(fab.iconProperty())
+				.listen();
+	}
+
+	@Override
+	protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return Region.USE_COMPUTED_SIZE;
+	}
+
+	@Override
+	protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+		MFXFabBase fab = getFab();
+		MFXFontIcon icon = fab.getIcon();
+		double iW = (icon != null) ? icon.getLayoutBounds().getWidth() : 0.0;
+		double w = fab.isExtended() ?
+				leftInset + iW + TextUtils.computeTextWidth(fab.getFont(), fab.getText()) + rightInset :
+				leftInset + iW + rightInset;
+		return Math.max(w, fab.getInitWidth());
+	}
+
+	@Override
+	protected void layoutChildren(double x, double y, double w, double h) {
+		MFXFabBase fab = getFab();
+		double lW = Math.max(LayoutUtils.boundWidth(label), w);
+		double lH = LayoutUtils.boundHeight(label);
+		HPos hPos = fab.isExtended() ? fab.getAlignment().getHpos() : HPos.CENTER;
+		VPos vPos = fab.getAlignment().getVpos();
+		Position lPos = LayoutUtils.computePosition(
+				fab, label,
+				x, y, w, h, 0, Insets.EMPTY,
+				hPos, vPos
+		);
+		label.resizeRelocate(lPos.getX(), lPos.getY(), lW, lH);
+		rg.resizeRelocate(0, 0, fab.getWidth(), fab.getHeight());
+	}
+
+	//================================================================================
+	// Getters
+	//================================================================================
+
+	/**
+	 * @return {@link #getSkinnable()} cast to {@link MFXFabBase}
+	 */
+	protected MFXFabBase getFab() {
+		return (MFXFabBase) getSkinnable();
+	}
+}

+ 228 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/theming/CSSFragment.java

@@ -0,0 +1,228 @@
+/*
+ * 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;
+
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+
+import java.util.Base64;
+import java.util.Objects;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Allows to build CSS stylesheets by code and apply them on any {@link Parent}.
+ */
+public class CSSFragment {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private static final Base64.Encoder enc = Base64.getEncoder();
+	private final String css;
+	private String converted;
+
+	public static final String DATA_URI_PREFIX = "data:base64,";
+
+	//================================================================================
+	// Constructors
+	//================================================================================
+	public CSSFragment(String css) {
+		this.css = css;
+	}
+
+	//================================================================================
+	// Static Methods
+	//================================================================================
+
+	/**
+	 * Applies the given CSS to the given {@link Parent}
+	 *
+	 * @see #applyOn(Parent)
+	 */
+	public static void applyOn(String css, Parent parent) {
+		CSSFragment f = new CSSFragment(css);
+		f.applyOn(parent);
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * Converts a CSS string to a data uri that can be used by JavaFX nodes to parse styles.
+	 * <p>
+	 * Subsequent calls to this will be faster as the converted CSS is cached.
+	 *
+	 * @see <a href="https://en.wikipedia.org/wiki/Data_URI_scheme">Data URI Scheme</a>
+	 */
+	public String toDataUri() {
+		if (converted == null) {
+			converted = DATA_URI_PREFIX + new String(enc.encode(css.getBytes(UTF_8)), UTF_8);
+		}
+		return converted;
+	}
+
+	/**
+	 * If this CSS fragment has not been applied yet to the given {@link Parent}, applies it
+	 * using {@link Parent#getStylesheets()}
+	 *
+	 * @see #isAppliedOn(Parent)
+	 */
+	public void applyOn(Parent parent) {
+		if (!isAppliedOn(parent))
+			parent.getStylesheets().add(toDataUri());
+	}
+
+	/**
+	 * If this CSS fragment has not been applied yet to the given {@link Scene}, applies it
+	 * using {@link Scene#getStylesheets()}.
+	 *
+	 * @see #isAppliedOn(Scene)
+	 */
+	public void applyOn(Scene scene) {
+		if (!isAppliedOn(scene))
+			scene.getStylesheets().add(toDataUri());
+	}
+
+	/**
+	 * Checks whether this CSS Fragment has already been applied to the given {@link Parent}
+	 * by checking if its stylesheets list contains this (converted to a Data URI).
+	 */
+	public boolean isAppliedOn(Parent parent) {
+		return parent.getStylesheets().contains(toDataUri());
+	}
+
+	/**
+	 * Checks whether this CSS Fragment has already been applied to the given {@link Scene}
+	 * by checking if its stylesheets list contains this (converted to a Data URI).
+	 */
+	public boolean isAppliedOn(Scene scene) {
+		return scene.getStylesheets().contains(toDataUri());
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+
+	@Override
+	public boolean equals(Object o) {
+		if (this == o) return true;
+		if (o == null || getClass() != o.getClass()) return false;
+		CSSFragment that = (CSSFragment) o;
+		return css.equals(that.css);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(css);
+	}
+
+	@Override
+	public String toString() {
+		return css;
+	}
+
+	//================================================================================
+	// Builder
+	//================================================================================
+
+	/**
+	 * Allows building {@code CSSFragments} with fluent API.
+	 * <p></p>
+	 * Usage example:
+	 * <pre>
+	 * {@code
+	 * Parent p = ...;
+	 * CssFragment.Builder.build()
+	 *     .addSelector("aSelector")
+	 *     .addStyle("-fx-background-color: red")
+	 *     .addStyle("-fx-background-radius: 10px")
+	 *     .closeSelector()
+	 *     .addSelector("aSelector:hover")
+	 *     .addStyle("-fx-background-color: orange")
+	 *     .addStyle("-fx-border-color: lightgray")
+	 *     .closeSelector()
+	 *     .applyOn(p);
+	 * }
+	 * </pre>
+	 */
+	public static class Builder {
+		private final StringBuilder sb = new StringBuilder();
+
+		public static Builder build() {
+			return new Builder();
+		}
+
+		/**
+		 * Use this to open a block for an element with its selector.
+		 * It's not needed to add the ending '{\n' as it is automatically added.
+		 * <p></p>
+		 * Once you finish the styling block for this element you must call {@link #closeSelector()}
+		 */
+		public Builder addSelector(String selector) {
+			sb.append(selector).append("{\n");
+			return this;
+		}
+
+		/**
+		 * Must always be called after {@link #addSelector(String)} to close the styling block for an element.
+		 */
+		public Builder closeSelector() {
+			sb.append("}\n\n");
+			return this;
+		}
+
+		/**
+		 * Adds the given style to the fragment.
+		 * It's not needed to add the ending ';\n' as it is automatically added.
+		 */
+		public Builder addStyle(String style) {
+			sb.append(style).append(";\n");
+			return this;
+		}
+
+		/**
+		 * Converts the built string to a {@link CSSFragment}.
+		 */
+		public CSSFragment toCSS() {
+			if (sb.length() == 0) throw new IllegalStateException("No styles set");
+			return new CSSFragment(sb.toString());
+		}
+
+		/**
+		 * Applies the created {@link CSSFragment} on the given {@link Parent}.
+		 *
+		 * @see #toCSS()
+		 * @see CSSFragment#applyOn(Parent)
+		 */
+		public void applyOn(Parent parent) {
+			toCSS().applyOn(parent);
+		}
+
+		/**
+		 * Applies the created {@link CSSFragment} on the given {@link Scene}.
+		 *
+		 * @see #toCSS()
+		 * @see CSSFragment#applyOn(Scene)
+		 */
+		public void applyOn(Scene scene) {
+			toCSS().applyOn(scene);
+		}
+	}
+}

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

@@ -18,14 +18,13 @@
 
 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}.
+ * Enumerator implementing {@link Variant} to define the variants of {@link MFXFab}.
  * <p></p>
- * Note that {@link MFXExtendedFab} doesn't have 'small' and 'large' variants, applying those will likely result
+ * Note that extended FABs doesn't have 'small' and 'large' variants, applying those will likely result
  * in un-styled components.
  */
 public enum FABVariants implements Variant {
@@ -35,6 +34,10 @@ public enum FABVariants implements Variant {
 	SURFACE("surface"),
 	SECONDARY("secondary"),
 	TERTIARY("tertiary"),
+	// TODO this is a good way of managing it without relying on PseudoClasses
+	// TODO the only improvement I'd make is to have a bunch of methods here responsible for applying the variant
+	// TODO 'fab-extended' should be applied only after 'fab' is removed
+	EXTENDED("fab-extended"),
 	;
 
 	private final String styleClass;

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

@@ -0,0 +1,53 @@
+/*
+ * 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.mfxresources.MFXResources;
+import javafx.scene.Scene;
+
+import java.net.URL;
+
+public enum MFXThemeManager {
+	LIGHT("sass/md3/mfx-light.css"),
+	;
+
+	private final String path;
+
+	MFXThemeManager(String path) {
+		this.path = path;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void addOn(Scene scene) {
+		String stylesheet = load();
+		if (!scene.getStylesheets().contains(stylesheet))
+			scene.getStylesheets().add(stylesheet);
+	}
+
+	public String load() {
+		return MFXResources.load(getPath());
+	}
+
+	public URL loadURL() {
+		return MFXResources.loadURL(getPath());
+	}
+}

+ 8 - 0
modules/components/src/test/java/app/ComponentsLauncher.java

@@ -24,6 +24,14 @@ import javafx.application.Application;
 public class ComponentsLauncher {
 
 	public static void main(String[] args) {
+		//System.setProperty("prism.order", "sw");
+		//System.setProperty("prism.text", "t2k");
+		//System.setProperty("prism.lcdtext", "false");
+		//System.setProperty("prism.vsync", "false");
+		//System.setProperty("prism.showdirty", "true");
+		//System.setProperty("prism.forceGPU","true");
+		//System.setProperty("prism.verbose", "true");
+		//System.setProperty("javafx.animation.fullspeed", "true");
 		Application.launch(ButtonsPlayground.class, args);
 	}
 }

+ 27 - 12
modules/components/src/test/java/app/buttons/ButtonsPlayground.java

@@ -22,7 +22,6 @@ 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;
@@ -38,9 +37,11 @@ import javafx.scene.Scene;
 import javafx.scene.control.ComboBox;
 import javafx.scene.control.ContentDisplay;
 import javafx.scene.control.ScrollPane;
+import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.BorderPane;
 import javafx.scene.layout.VBox;
 import javafx.stage.Stage;
+import org.scenicview.ScenicView;
 
 import java.util.function.BiFunction;
 
@@ -80,7 +81,7 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		stage.setTitle("Buttons Playground");
 		stage.show();
 
-		//ScenicView.show(scene);
+		ScenicView.show(scene);
 	}
 
 	@Override
@@ -141,13 +142,19 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 	}
 
 	private Node extendedFabView() {
+		BiFunction<String, MFXFontIcon, MFXFab> generator = (s, i) -> {
+			MFXFab fab = MFXFab.extended();
+			fab.setText(s);
+			fab.setIcon(i);
+			return fab;
+		};
 		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));
+		Node def = createExtendedFabView("Extended FABs", generator);
+		Node surf = createExtendedFabView("Extended FABs (Surface)", generator.andThen(f -> f.setVariants(FABVariants.SURFACE)));
+		Node sdy = createExtendedFabView("Extended FABs (Secondary)", generator.andThen(f -> f.setVariants(FABVariants.SECONDARY)));
+		Node tty = createExtendedFabView("Extended FABs (Tertiary)", generator.andThen(f -> f.setVariants(FABVariants.TERTIARY)));
 		box.getChildren().addAll(def, surf, sdy, tty);
 		return box;
 	}
@@ -211,15 +218,20 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		btn7.addVariants(FABVariants.LARGE, FABVariants.LOWERED);
 		btn8.addVariants(FABVariants.LOWERED, FABVariants.LARGE);
 
+		btn8.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
+			btn8.getFabBehavior().ifPresent(b -> b.changeIcon(randomIcon(FONTAWESOME_SOLID)));
+			e.consume();
+		});
+
 		defTfp.add(btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8);
 		return defTfp;
 	}
 
-	private Node createExtendedFabView(String title, BiFunction<String, MFXFontIcon, MFXExtendedFab> generator) {
+	private Node createExtendedFabView(String title, BiFunction<String, MFXFontIcon, MFXFab> generator) {
 		return createExtendedFabView(title, 700, generator);
 	}
 
-	private Node createExtendedFabView(String title, double length, BiFunction<String, MFXFontIcon, MFXExtendedFab> generator) {
+	private Node createExtendedFabView(String title, double length, BiFunction<String, MFXFontIcon, MFXFab> generator) {
 		TitledFlowPane defTfp = new TitledFlowPane(title);
 		defTfp.setMaxWidth(length);
 
@@ -228,10 +240,10 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		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));
+		MFXFab btn5 = generator.apply("Text Only", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn6 = generator.apply("Icon to Right", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn7 = generator.apply("Lowered Text Only", randomIcon(FONTAWESOME_SOLID));
+		MFXFab btn8 = generator.apply("Lowered Icon to Right", randomIcon(FONTAWESOME_SOLID));
 
 		btn1.setDisable(true);
 		btn2.setMouseTransparent(true);
@@ -248,6 +260,9 @@ public class ButtonsPlayground extends Application implements MultipleViewApp<St
 		btn8.addVariants(FABVariants.LOWERED);
 		btn8.setContentDisplay(ContentDisplay.RIGHT);
 
+		btn6.setExtended(false);
+		btn6.setOnAction(e -> btn6.setExtended(!btn6.isExtended()));
+
 		defTfp.add(btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8);
 		return defTfp;
 	}

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

@@ -75,7 +75,7 @@ $pmy_text: 'on-primary-container';
 $pmy_icon: 'on-primary-container';
 $pmy_ripple: 'on-primary-container';
 
-.mfx-button.extended-fab {
+.mfx-button.fab-extended {
   @include fab_base($pmy_surface, $pmy_tint, $pmy_text, 3);
   @include fab_disabled();
   @include fab_hover($pmy_surface, $pmy_tint, 4);
@@ -94,7 +94,7 @@ $sfe_text: 'primary';
 $sfe_icon: 'primary';
 $sfe_ripple: 'primary';
 
-.mfx-button.extended-fab.surface {
+.mfx-button.fab-extended.surface {
   @include fab_base($sfe_surface, $sfe_tint, $sfe_text, 3);
   @include fab_disabled();
   @include fab_hover($sfe_surface, $sfe_tint, 4);
@@ -113,7 +113,7 @@ $sdy_text: 'on-secondary-container';
 $sdy_icon: 'on-secondary-container';
 $sdy_ripple: 'on-secondary-container';
 
-.mfx-button.extended-fab.secondary {
+.mfx-button.fab-extended.secondary {
   @include fab_base($sdy_surface, $sdy_tint, $sdy_text, 3);
   @include fab_disabled();
   @include fab_hover($sdy_surface, $sdy_tint, 4);
@@ -132,7 +132,7 @@ $tty_text: 'on-tertiary-container';
 $tty_icon: 'on-tertiary-container';
 $tty_ripple: 'on-tertiary-container';
 
-.mfx-button.extended-fab.tertiary {
+.mfx-button.fab-extended.tertiary {
   @include fab_base($tty_surface, $tty_tint, $tty_text, 3);
   @include fab_disabled();
   @include fab_hover($tty_surface, $tty_tint, 4);

+ 52 - 52
modules/resources/src/main/resources/io/github/palexdev/mfxresources/sass/md3/mfx-light.css

@@ -556,7 +556,7 @@
 /****************************************************************************************************
  * Extended FAB Primary
  ****************************************************************************************************/
-.mfx-button.extended-fab {
+.mfx-button.fab-extended {
   -fx-font-family: "Roboto Medium";
   -fx-font-weight: 500;
   -fx-font-size: 14px;
@@ -570,55 +570,55 @@
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab:disabled > .label {
+.mfx-button.fab-extended:disabled > .label {
   -fx-opacity: 1;
 }
 
-.mfx-button.extended-fab:hover {
+.mfx-button.fab-extended:hover {
   -fx-background-color: #EADDFF, rgba(33, 0, 93, 0.08);
   -mfx-elevation: LEVEL4;
 }
 
-.mfx-button.extended-fab:focused,
-.mfx-button.extended-fab:pressed {
+.mfx-button.fab-extended:focused,
+.mfx-button.fab-extended:pressed {
   -fx-background-color: #EADDFF, rgba(33, 0, 93, 0.12);
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab:with-icon-left {
+.mfx-button.fab-extended:with-icon-left {
   -fx-padding: 0px 20px 0px 16px;
 }
 
-.mfx-button.extended-fab:with-icon-right {
+.mfx-button.fab-extended:with-icon-right {
   -fx-padding: 0px 16px 0px 20px;
 }
 
-.mfx-button.extended-fab .mfx-font-icon {
+.mfx-button.fab-extended .mfx-font-icon {
   -mfx-color: #21005D;
   -mfx-size: 24px;
 }
 
-.mfx-button.extended-fab .mfx-ripple-generator {
+.mfx-button.fab-extended .mfx-ripple-generator {
   -mfx-ripple-color: rgba(33, 0, 93, 0.12);
 }
 
-.mfx-button.extended-fab.lowered {
+.mfx-button.fab-extended.lowered {
   -mfx-elevation: LEVEL1;
 }
 
-.mfx-button.extended-fab.lowered:hover {
+.mfx-button.fab-extended.lowered:hover {
   -mfx-elevation: LEVEL2;
 }
 
-.mfx-button.extended-fab.lowered:focused,
-.mfx-button.extended-fab.lowered:pressed {
+.mfx-button.fab-extended.lowered:focused,
+.mfx-button.fab-extended.lowered:pressed {
   -mfx-elevation: LEVEL1;
 }
 
 /****************************************************************************************************
  * Extended FAB Surface
  ****************************************************************************************************/
-.mfx-button.extended-fab.surface {
+.mfx-button.fab-extended.surface {
   -fx-font-family: "Roboto Medium";
   -fx-font-weight: 500;
   -fx-font-size: 14px;
@@ -632,55 +632,55 @@
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab.surface:disabled > .label {
+.mfx-button.fab-extended.surface:disabled > .label {
   -fx-opacity: 1;
 }
 
-.mfx-button.extended-fab.surface:hover {
+.mfx-button.fab-extended.surface:hover {
   -fx-background-color: #FFFBFE, rgba(103, 80, 164, 0.08);
   -mfx-elevation: LEVEL4;
 }
 
-.mfx-button.extended-fab.surface:focused,
-.mfx-button.extended-fab.surface:pressed {
+.mfx-button.fab-extended.surface:focused,
+.mfx-button.fab-extended.surface:pressed {
   -fx-background-color: #FFFBFE, rgba(103, 80, 164, 0.12);
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab.surface:with-icon-left {
+.mfx-button.fab-extended.surface:with-icon-left {
   -fx-padding: 0px 20px 0px 16px;
 }
 
-.mfx-button.extended-fab.surface:with-icon-right {
+.mfx-button.fab-extended.surface:with-icon-right {
   -fx-padding: 0px 16px 0px 20px;
 }
 
-.mfx-button.extended-fab.surface .mfx-font-icon {
+.mfx-button.fab-extended.surface .mfx-font-icon {
   -mfx-color: #6750A4;
   -mfx-size: 24px;
 }
 
-.mfx-button.extended-fab.surface .mfx-ripple-generator {
+.mfx-button.fab-extended.surface .mfx-ripple-generator {
   -mfx-ripple-color: rgba(103, 80, 164, 0.12);
 }
 
-.mfx-button.extended-fab.surface.lowered {
+.mfx-button.fab-extended.surface.lowered {
   -mfx-elevation: LEVEL1;
 }
 
-.mfx-button.extended-fab.surface.lowered:hover {
+.mfx-button.fab-extended.surface.lowered:hover {
   -mfx-elevation: LEVEL2;
 }
 
-.mfx-button.extended-fab.surface.lowered:focused,
-.mfx-button.extended-fab.surface.lowered:pressed {
+.mfx-button.fab-extended.surface.lowered:focused,
+.mfx-button.fab-extended.surface.lowered:pressed {
   -mfx-elevation: LEVEL1;
 }
 
 /****************************************************************************************************
  * Extended FAB Secondary
  ****************************************************************************************************/
-.mfx-button.extended-fab.secondary {
+.mfx-button.fab-extended.secondary {
   -fx-font-family: "Roboto Medium";
   -fx-font-weight: 500;
   -fx-font-size: 14px;
@@ -694,55 +694,55 @@
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab.secondary:disabled > .label {
+.mfx-button.fab-extended.secondary:disabled > .label {
   -fx-opacity: 1;
 }
 
-.mfx-button.extended-fab.secondary:hover {
+.mfx-button.fab-extended.secondary:hover {
   -fx-background-color: #E8DEF8, rgba(29, 25, 43, 0.08);
   -mfx-elevation: LEVEL4;
 }
 
-.mfx-button.extended-fab.secondary:focused,
-.mfx-button.extended-fab.secondary:pressed {
+.mfx-button.fab-extended.secondary:focused,
+.mfx-button.fab-extended.secondary:pressed {
   -fx-background-color: #E8DEF8, rgba(29, 25, 43, 0.12);
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab.secondary:with-icon-left {
+.mfx-button.fab-extended.secondary:with-icon-left {
   -fx-padding: 0px 20px 0px 16px;
 }
 
-.mfx-button.extended-fab.secondary:with-icon-right {
+.mfx-button.fab-extended.secondary:with-icon-right {
   -fx-padding: 0px 16px 0px 20px;
 }
 
-.mfx-button.extended-fab.secondary .mfx-font-icon {
+.mfx-button.fab-extended.secondary .mfx-font-icon {
   -mfx-color: #1D192B;
   -mfx-size: 24px;
 }
 
-.mfx-button.extended-fab.secondary .mfx-ripple-generator {
+.mfx-button.fab-extended.secondary .mfx-ripple-generator {
   -mfx-ripple-color: rgba(29, 25, 43, 0.12);
 }
 
-.mfx-button.extended-fab.secondary.lowered {
+.mfx-button.fab-extended.secondary.lowered {
   -mfx-elevation: LEVEL1;
 }
 
-.mfx-button.extended-fab.secondary.lowered:hover {
+.mfx-button.fab-extended.secondary.lowered:hover {
   -mfx-elevation: LEVEL2;
 }
 
-.mfx-button.extended-fab.secondary.lowered:focused,
-.mfx-button.extended-fab.secondary.lowered:pressed {
+.mfx-button.fab-extended.secondary.lowered:focused,
+.mfx-button.fab-extended.secondary.lowered:pressed {
   -mfx-elevation: LEVEL1;
 }
 
 /****************************************************************************************************
  * Extended FAB Tertiary
  ****************************************************************************************************/
-.mfx-button.extended-fab.tertiary {
+.mfx-button.fab-extended.tertiary {
   -fx-font-family: "Roboto Medium";
   -fx-font-weight: 500;
   -fx-font-size: 14px;
@@ -756,48 +756,48 @@
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab.tertiary:disabled > .label {
+.mfx-button.fab-extended.tertiary:disabled > .label {
   -fx-opacity: 1;
 }
 
-.mfx-button.extended-fab.tertiary:hover {
+.mfx-button.fab-extended.tertiary:hover {
   -fx-background-color: #FFD8E4, rgba(49, 17, 29, 0.08);
   -mfx-elevation: LEVEL4;
 }
 
-.mfx-button.extended-fab.tertiary:focused,
-.mfx-button.extended-fab.tertiary:pressed {
+.mfx-button.fab-extended.tertiary:focused,
+.mfx-button.fab-extended.tertiary:pressed {
   -fx-background-color: #FFD8E4, rgba(49, 17, 29, 0.12);
   -mfx-elevation: LEVEL3;
 }
 
-.mfx-button.extended-fab.tertiary:with-icon-left {
+.mfx-button.fab-extended.tertiary:with-icon-left {
   -fx-padding: 0px 20px 0px 16px;
 }
 
-.mfx-button.extended-fab.tertiary:with-icon-right {
+.mfx-button.fab-extended.tertiary:with-icon-right {
   -fx-padding: 0px 16px 0px 20px;
 }
 
-.mfx-button.extended-fab.tertiary .mfx-font-icon {
+.mfx-button.fab-extended.tertiary .mfx-font-icon {
   -mfx-color: #31111D;
   -mfx-size: 24px;
 }
 
-.mfx-button.extended-fab.tertiary .mfx-ripple-generator {
+.mfx-button.fab-extended.tertiary .mfx-ripple-generator {
   -mfx-ripple-color: rgba(49, 17, 29, 0.12);
 }
 
-.mfx-button.extended-fab.tertiary.lowered {
+.mfx-button.fab-extended.tertiary.lowered {
   -mfx-elevation: LEVEL1;
 }
 
-.mfx-button.extended-fab.tertiary.lowered:hover {
+.mfx-button.fab-extended.tertiary.lowered:hover {
   -mfx-elevation: LEVEL2;
 }
 
-.mfx-button.extended-fab.tertiary.lowered:focused,
-.mfx-button.extended-fab.tertiary.lowered:pressed {
+.mfx-button.fab-extended.tertiary.lowered:focused,
+.mfx-button.fab-extended.tertiary.lowered:pressed {
   -mfx-elevation: LEVEL1;
 }