瀏覽代碼

:arrow_up: Upgrade components module

Controls Package
:bug: Fixed critical bug related to the initWidth and initHeight new styleable properties. For some reason they were causing the CSS to be reapplied continuously, causing memory leaks and a huge performance hit over time. Override invalidated() instead on set(...)
:recycle: Make both MFXControl and MFXLabeled implement the new MFXResizable API for integration with LayoutStrategy
:recycle: MFXSkinBase: make size computation methods public for integration with MFXResizable and LayoutStrategy
:recycle: Reworked and renamed applyInitSizes(...) to onInitSizesChanged(). Init sizes now are taken into account by the new LayoutStrategy API

Layout Package
:boom: New API to allow MaterialFX controls to easily change their sizing/layout strategy by simply setting a property instead of creating custom skins, custom components or overriding methods inline

Skins Package
:recycle: MFXFabSkin: do not take into account the init width as it is used by the LayoutStrategy now

Tests
:white_check_mark: Added tests for the new LayoutStrategy and MFXResizable APIs

Signed-off-by: Alessadro Parisi <alessandro.parisi406@gmail.com>
Alessadro Parisi 2 年之前
父節點
當前提交
4f52c297d2

+ 11 - 3
.idea/session-manager.json

@@ -1,10 +1,18 @@
 [
   {
     "name": "Default",
-    "desc": "Write documentation for the new added Curves, and document the all the other related changes (add videos from Flutter too)",
-    "fFile": "/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/motion/Motion.java",
+    "desc": "I was finishing the tests on the new LayoutStrategy API",
+    "fFile": "/modules/components/src/test/java/interactive/TestLayoutStrategies.java",
     "files": [
-      "/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/motion/Motion.java"
+      "/modules/components/src/main/java/io/github/palexdev/mfxcomponents/layout/MFXResizable.java",
+      "/modules/components/src/test/java/interactive/TestLayoutStrategies.java",
+      "/modules/components/src/test/java/interactive/TestInitSize.java",
+      "/modules/core/src/main/java/io/github/palexdev/mfxcore/utils/fx/LayoutUtils.java",
+      "/modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXFabBehavior.java",
+      "/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXControl.java",
+      "/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/base/MFXLabeled.java",
+      "/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/fab/MFXFabBase.java",
+      "/modules/components/src/main/java/io/github/palexdev/mfxcomponents/layout/LayoutStrategy.java"
     ]
   }
 ]

+ 14 - 0
modules/components/CHANGELOG.md

@@ -27,6 +27,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - 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
+- New API to allow MaterialFX controls to easily change their sizing/layout strategy by simply setting a property
+  instead of creating custom skins, custom components or overriding methods inline
+- Added tests for the new LayoutStrategy and MFXResizable APIs
 
 ## Changed
 
@@ -44,11 +47,22 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
   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)
+- Make both MFXControl and MFXLabeled implement the new MFXResizable API for integration with LayoutStrategy
+- MFXSkinBase: make size computation methods public for integration with MFXResizable and LayoutStrategy
+- Reworked and renamed applyInitSizes(...) to onInitSizesChanged(). Init sizes now are taken into account by the new
+  LayoutStrategy API
+- MFXFabSkin: do not take into account the init width as it is used by the LayoutStrategy now
 
 ## Removed
 
 - MFXExtendedFab has been removed to avoid code duplication
 
+## Fixed
+
+- Fixed critical bug related to the initWidth and initHeight new styleable properties. For some reason they were causing
+  the CSS to be reapplied continuously, causing memory leaks and a huge performance hit over time. Override
+  invalidated() instead on set(...)
+
 ## [11.16.0] - 09-02-2023
 
 ## Added

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

@@ -177,7 +177,7 @@ public class MFXFabBehavior extends MFXButtonBehavior {
 	@SuppressWarnings("JavadocReference")
 	protected double computeWidth() {
 		MFXFabBase fab = getFab();
-		return fab.computePrefWidth(-1);
+		return fab.computePrefWidth(fab.getHeight());
 	}
 
 	/**

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

@@ -19,11 +19,15 @@
 package io.github.palexdev.mfxcomponents.controls.base;
 
 import io.github.palexdev.mfxcomponents.behaviors.MFXFabBehavior;
+import io.github.palexdev.mfxcomponents.layout.LayoutStrategy;
+import io.github.palexdev.mfxcomponents.layout.MFXResizable;
 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.utils.fx.StyleUtils;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.css.CssMetaData;
 import javafx.css.Styleable;
 import javafx.css.StyleablePropertyFactory;
@@ -37,7 +41,7 @@ import java.util.function.Supplier;
  * Base class for MaterialFX controls. The idea is to have a separate hierarchy of components from the JavaFX one,
  * that perfectly integrates with the new Behavior and Theming APIs.
  * <p>
- * Extends {@link Control} and implements both {@link WithBehavior} and {@link MFXStyleable}.
+ * Extends {@link Control} and implements, {@link WithBehavior}, {@link MFXStyleable} and {@link MFXResizable}.
  * 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>
@@ -59,11 +63,16 @@ import java.util.function.Supplier;
  * <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()}.
+ * <p>
+ * Since this always implements {@link MFXResizable}, it redefines the JavaFX's layout strategy by extending it to take
+ * into account the aforementioned sizes.
  *
  * @param <B> the behavior type used by the control
+ * @see MFXSkinBase
+ * @see MFXResizable
  */
 @SuppressWarnings({"unchecked", "rawtypes"})
-public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends Control implements WithBehavior<B>, MFXStyleable {
+public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends Control implements WithBehavior<B>, MFXStyleable, MFXResizable {
 	//================================================================================
 	// Properties
 	//================================================================================
@@ -80,6 +89,13 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 		}
 	};
 
+	private final ObjectProperty<LayoutStrategy> layoutStrategy = new SimpleObjectProperty<>(LayoutStrategy.defaultStrategy()) {
+		@Override
+		protected void invalidated() {
+			onLayoutStrategyChanged();
+		}
+	};
+
 	//================================================================================
 	// Abstract Methods
 	//================================================================================
@@ -90,20 +106,14 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 
 	/**
-	 * 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.
+	 * This is automatically invoked when either {@link #initHeightProperty()} or {@link #initWidthProperty()} change.
+	 * By default, this method triggers a layout request.
 	 * <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.
+	 * The consequence is that the current set {@link LayoutStrategy} will be used to re-compute the component's sizes, and
+	 * if it takes into account those init sizes, the component will resize accordingly.
 	 */
-	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);
+	protected void onInitSizesChanged() {
+		requestLayout();
 	}
 
 	/**
@@ -116,34 +126,39 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
+	@Override
+	public void onLayoutStrategyChanged() {
+		requestLayout();
+	}
+
 	@Override
 	public double computeMinWidth(double height) {
-		return super.computeMinWidth(height);
+		return getLayoutStrategy().computeMinWidth(this);
 	}
 
 	@Override
 	public double computeMinHeight(double width) {
-		return super.computeMinHeight(width);
+		return getLayoutStrategy().computeMinHeight(this);
 	}
 
 	@Override
 	public double computePrefWidth(double height) {
-		return super.computePrefWidth(height);
+		return getLayoutStrategy().computePrefWidth(this);
 	}
 
 	@Override
 	public double computePrefHeight(double width) {
-		return super.computePrefHeight(width);
+		return getLayoutStrategy().computePrefHeight(this);
 	}
 
 	@Override
 	public double computeMaxWidth(double height) {
-		return super.computeMaxWidth(height);
+		return getLayoutStrategy().computeMaxWidth(this);
 	}
 
 	@Override
 	public double computeMaxHeight(double width) {
-		return super.computeMaxHeight(width);
+		return getLayoutStrategy().computeMaxHeight(this);
 	}
 
 	@Override
@@ -163,9 +178,8 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 			USE_COMPUTED_SIZE
 	) {
 		@Override
-		public void set(double v) {
-			super.set(v);
-			applyInitSizes(false);
+		public void invalidated() {
+			onInitSizesChanged();
 		}
 	};
 
@@ -176,9 +190,8 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 			USE_COMPUTED_SIZE
 	) {
 		@Override
-		public void set(double v) {
-			super.set(v);
-			applyInitSizes(false);
+		public void invalidated() {
+			onInitSizesChanged();
 		}
 	};
 
@@ -187,7 +200,7 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	}
 
 	/**
-	 * Specifies the component's initial height when created.
+	 * Specifies the component's initial height upon creation.
 	 * <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
@@ -195,7 +208,8 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	 * 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)}.
+	 * The way initial sizes are applied depends on the set {@link LayoutStrategy}, when this changes the layout request
+	 * is automatically triggered by {@link #onInitSizesChanged()}.
 	 * <p></p>
 	 * Can be set in CSS via the property: '-mfx-init-height'.
 	 */
@@ -220,7 +234,8 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	 * 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)}.
+	 * The way initial sizes are applied depends on the set {@link LayoutStrategy}, when this changes the layout request
+	 * is automatically triggered by {@link #onInitSizesChanged()}.
 	 * <p></p>
 	 * Can be set in CSS via the property: '-mfx-init-width'.
 	 */
@@ -292,4 +307,19 @@ public abstract class MFXControl<B extends BehaviorBase<? extends Node>> extends
 	public void setBehaviorProvider(Supplier<B> behaviorProvider) {
 		this.behaviorProvider.set(behaviorProvider);
 	}
+
+	@Override
+	public LayoutStrategy getLayoutStrategy() {
+		return layoutStrategy.get();
+	}
+
+	@Override
+	public ObjectProperty<LayoutStrategy> layoutStrategyProperty() {
+		return layoutStrategy;
+	}
+
+	@Override
+	public void setLayoutStrategy(LayoutStrategy layoutStrategy) {
+		this.layoutStrategy.set(layoutStrategy);
+	}
 }

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

@@ -19,11 +19,16 @@
 package io.github.palexdev.mfxcomponents.controls.base;
 
 import io.github.palexdev.mfxcomponents.behaviors.MFXFabBehavior;
+import io.github.palexdev.mfxcomponents.layout.LayoutStrategy;
+import io.github.palexdev.mfxcomponents.layout.LayoutStrategy.Defaults;
+import io.github.palexdev.mfxcomponents.layout.MFXResizable;
 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.utils.fx.StyleUtils;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.css.CssMetaData;
 import javafx.css.Styleable;
 import javafx.css.StyleablePropertyFactory;
@@ -37,7 +42,7 @@ 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, Skin and Theming APIs.
  * <p>
- * Extends {@link Labeled} and implements both {@link WithBehavior} and {@link MFXStyleable}.
+ * Extends {@link Labeled} and implements {@link WithBehavior}, {@link MFXStyleable} and {@link MFXResizable}.
  * 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>
@@ -57,12 +62,16 @@ import java.util.function.Supplier;
  * <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()}.
+ * <p>
+ * Since this always implements {@link MFXResizable}, it redefines the JavaFX's layout strategy by extending it to take
+ * into account the aforementioned sizes.
  *
  * @param <B> the behavior type used by the control
  * @see MFXSkinBase
+ * @see MFXResizable
  */
 @SuppressWarnings({"unchecked", "rawtypes"})
-public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends Labeled implements WithBehavior<B>, MFXStyleable {
+public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends Labeled implements WithBehavior<B>, MFXStyleable, MFXResizable {
 	//================================================================================
 	// Properties
 	//================================================================================
@@ -79,6 +88,13 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 		}
 	};
 
+	private final ObjectProperty<LayoutStrategy> layoutStrategy = new SimpleObjectProperty<>(defaultLayoutStrategy()) {
+		@Override
+		protected void invalidated() {
+			onLayoutStrategyChanged();
+		}
+	};
+
 	//================================================================================
 	// Constructors
 	//================================================================================
@@ -107,20 +123,14 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 
 	/**
-	 * 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.
+	 * This is automatically invoked when either {@link #initHeightProperty()} or {@link #initWidthProperty()} change.
+	 * By default, this method triggers a layout request.
 	 * <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.
+	 * The consequence is that the current set {@link LayoutStrategy} will be used to re-compute the component's sizes, and
+	 * if it takes into account those init sizes, the component will resize accordingly.
 	 */
-	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);
+	protected void onInitSizesChanged() {
+		requestLayout();
 	}
 
 	/**
@@ -133,34 +143,46 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
+	@Override
+	public void onLayoutStrategyChanged() {
+		requestLayout();
+	}
+
+	@Override
+	public LayoutStrategy defaultLayoutStrategy() {
+		return LayoutStrategy.defaultStrategy()
+				.setPrefWidthFunction(Defaults.DEF_PREF_WIDTH_FUNCTION.andThen(r -> Math.max(r, getInitWidth())))
+				.setPrefHeightFunction(Defaults.DEF_PREF_HEIGHT_FUNCTION.andThen(r -> Math.max(r, getInitHeight())));
+	}
+
 	@Override
 	public double computeMinWidth(double height) {
-		return super.computeMinWidth(height);
+		return getLayoutStrategy().computeMinWidth(this);
 	}
 
 	@Override
 	public double computeMinHeight(double width) {
-		return super.computeMinHeight(width);
+		return getLayoutStrategy().computeMinHeight(this);
 	}
 
 	@Override
 	public double computePrefWidth(double height) {
-		return super.computePrefWidth(height);
+		return getLayoutStrategy().computePrefWidth(this);
 	}
 
 	@Override
 	public double computePrefHeight(double width) {
-		return super.computePrefHeight(width);
+		return getLayoutStrategy().computePrefHeight(this);
 	}
 
 	@Override
 	public double computeMaxWidth(double height) {
-		return super.computeMaxWidth(height);
+		return getLayoutStrategy().computeMaxWidth(this);
 	}
 
 	@Override
 	public double computeMaxHeight(double width) {
-		return super.computeMaxHeight(width);
+		return getLayoutStrategy().computeMaxHeight(this);
 	}
 
 	/**
@@ -180,17 +202,6 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	//================================================================================
 	// 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,
@@ -198,9 +209,8 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 			USE_COMPUTED_SIZE
 	) {
 		@Override
-		public void set(double v) {
-			super.set(v);
-			applyInitSizes(false);
+		public void invalidated() {
+			onInitSizesChanged();
 		}
 	};
 
@@ -211,9 +221,8 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 			USE_COMPUTED_SIZE
 	) {
 		@Override
-		public void set(double v) {
-			super.set(v);
-			applyInitSizes(false);
+		public void invalidated() {
+			onInitSizesChanged();
 		}
 	};
 
@@ -237,7 +246,8 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	 * 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)}.
+	 * The way initial sizes are applied depends on the set {@link LayoutStrategy}, when this changes the layout request
+	 * is automatically triggered by {@link #onInitSizesChanged()}.
 	 * <p></p>
 	 * Can be set in CSS via the property: '-mfx-init-height'.
 	 */
@@ -262,7 +272,8 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	 * 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)}.
+	 * The way initial sizes are applied depends on the set {@link LayoutStrategy}, when this changes the layout request
+	 * is automatically triggered by {@link #onInitSizesChanged()}.
 	 * <p></p>
 	 * Can be set in CSS via the property: '-mfx-init-width'.
 	 */
@@ -359,4 +370,19 @@ public abstract class MFXLabeled<B extends BehaviorBase<? extends Node>> extends
 	public void setBehaviorProvider(Supplier<B> behaviorProvider) {
 		this.behaviorProvider.set(behaviorProvider);
 	}
+
+	@Override
+	public LayoutStrategy getLayoutStrategy() {
+		return layoutStrategy.get();
+	}
+
+	@Override
+	public ObjectProperty<LayoutStrategy> layoutStrategyProperty() {
+		return layoutStrategy;
+	}
+
+	@Override
+	public void setLayoutStrategy(LayoutStrategy layoutStrategy) {
+		this.layoutStrategy.set(layoutStrategy);
+	}
 }

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

@@ -18,6 +18,7 @@
 
 package io.github.palexdev.mfxcomponents.controls.base;
 
+import io.github.palexdev.mfxcomponents.layout.MFXResizable;
 import io.github.palexdev.mfxcore.behavior.BehaviorBase;
 import io.github.palexdev.mfxcore.behavior.WithBehavior;
 import javafx.beans.InvalidationListener;
@@ -43,7 +44,7 @@ import java.util.Optional;
  * <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
+ * The behavior may need to be connected with the specs of the component, as well as with the 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
@@ -57,6 +58,9 @@ import java.util.Optional;
  * <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
+ * <p></p>
+ * Last but not least, this skin makes all the methods responsible for computing the component' sizes {@code public}, this
+ * is for the integration with the {@link MFXResizable} API.
  */
 public abstract class MFXSkinBase<C extends Control & WithBehavior<B>, B extends BehaviorBase<C>> extends javafx.scene.control.SkinBase<C> {
 
@@ -119,6 +123,41 @@ public abstract class MFXSkinBase<C extends Control & WithBehavior<B>, B extends
 		Optional.ofNullable(getBehavior()).ifPresent(b -> b.filter(node, eventType, handler));
 	}
 
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+
+	@Override
+	public double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return super.computeMinWidth(height, topInset, rightInset, bottomInset, leftInset);
+	}
+
+	@Override
+	public double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
+	}
+
+	@Override
+	public double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
+	}
+
+	@Override
+	public double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
+	}
+
+	@Override
+	public double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+	}
+
+	@Override
+	public double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+		return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+	}
+
+
 	//================================================================================
 	// Getters/Setters
 	//================================================================================

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

@@ -117,14 +117,14 @@ public class MFXFab extends MFXFabBase implements WithVariants<MFXFab, FABVarian
 	@Override
 	public MFXFab addVariants(FABVariants... variants) {
 		WithVariants.addVariants(this, variants);
-		applyInitSizes(true);
+		onInitSizesChanged();
 		return this;
 	}
 
 	@Override
 	public MFXFab setVariants(FABVariants... variants) {
 		WithVariants.setVariants(this, variants);
-		applyInitSizes(true);
+		onInitSizesChanged();
 		return this;
 	}
 

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

@@ -140,9 +140,16 @@ public class MFXFabBase extends MFXElevatedButton {
 	//================================================================================
 	// Overridden Methods
 	//================================================================================
+
+	@Override
+	public void onLayoutStrategyChanged() {
+		if (getSkin() == null) return;
+		getFabBehavior().ifPresent(b -> b.extend(false));
+	}
+
 	@Override
-	protected void applyInitSizes(boolean force) {
-		super.applyInitSizes(force);
+	protected void onInitSizesChanged() {
+		super.onInitSizesChanged();
 
 		// This usually happens when the FAB changes between standard and extended
 		// In such cases it's important to ensure minimum sizes are correct

+ 277 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/layout/LayoutStrategy.java

@@ -0,0 +1,277 @@
+/*
+ * 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.layout;
+
+import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
+import io.github.palexdev.mfxcore.base.TriFunction;
+import io.github.palexdev.mfxcore.builders.InsetsBuilder;
+import javafx.geometry.Insets;
+import javafx.scene.control.Skin;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A {@code LayoutStrategy} defines a series of {@link Function}s that are responsible for computing:
+ * <p> 1) The minimum width
+ * <p> 2) The minimum height
+ * <p> 3) The preferred width
+ * <p> 4) The preferred height
+ * <p> 5) The maximum width
+ * <p> 6) The maximum height
+ * <p>
+ * ...of a component that implements {@link MFXResizable}.
+ * <p></p>
+ * An internal class, {@link Defaults}, offers a series of default layout functions that replicate the JavaFX's
+ * algorithm.
+ * <p></p>
+ * A good usage of this is to start from the defaults and then extend them using the {@link Function#andThen(Function)}
+ * feature.
+ * <p></p>
+ * When creating a new {@code LayoutStrategy} object through no-arg constructor or {@link #defaultStrategy()}, the six
+ * functions are set to the one in {@link Defaults} (JavaFX algorithm).
+ */
+public class LayoutStrategy {
+	//================================================================================
+	// Properties
+	//================================================================================
+	private Function<MFXResizable, Double> minWidthFunction = Defaults.DEF_MIN_WIDTH_FUNCTION;
+	private Function<MFXResizable, Double> minHeightFunction = Defaults.DEF_MIN_HEIGHT_FUNCTION;
+	private Function<MFXResizable, Double> prefWidthFunction = Defaults.DEF_PREF_WIDTH_FUNCTION;
+	private Function<MFXResizable, Double> prefHeightFunction = Defaults.DEF_PREF_HEIGHT_FUNCTION;
+	private Function<MFXResizable, Double> maxWidthFunction = Defaults.DEF_MAX_WIDTH_FUNCTION;
+	private Function<MFXResizable, Double> maxHeightFunction = Defaults.DEF_MAX_HEIGHT_FUNCTION;
+
+	//================================================================================
+	// Static Methods
+	//================================================================================
+
+	/**
+	 * @return a new {@code LayoutStrategy} instance that uses JavaFX's algorithm for all sizes
+	 */
+	public static LayoutStrategy defaultStrategy() {
+		return new LayoutStrategy();
+	}
+
+	//================================================================================
+	// Methods
+	//================================================================================
+
+	/**
+	 * Computes the minimum width for the specified {@link MFXResizable} by invoking the set {@link #getMinWidthFunction()}.
+	 *
+	 * @return the computed width or 0.0 if the function is null
+	 */
+	public double computeMinWidth(MFXResizable resizable) {
+		return minWidthFunction != null ? minWidthFunction.apply(resizable) : 0.0;
+	}
+
+	/**
+	 * Computes the minimum height for the specified {@link MFXResizable} by invoking the set {@link #getMinHeightFunction()}.
+	 *
+	 * @return the computed height or 0.0 if the function is null
+	 */
+	public double computeMinHeight(MFXResizable resizable) {
+		return minHeightFunction != null ? minHeightFunction.apply(resizable) : 0.0;
+	}
+
+	/**
+	 * Computes the preferred width for the specified {@link MFXResizable} by invoking the set {@link #getPrefWidthFunction()}.
+	 *
+	 * @return the computed width or 0.0 if the function is null
+	 */
+	public double computePrefWidth(MFXResizable resizable) {
+		return prefWidthFunction != null ? prefWidthFunction.apply(resizable) : 0.0;
+	}
+
+	/**
+	 * Computes the preferred height for the specified {@link MFXResizable} by invoking the set {@link #getPrefHeightFunction()}.
+	 *
+	 * @return the computed height or 0.0 if the function is null
+	 */
+	public double computePrefHeight(MFXResizable resizable) {
+		return prefHeightFunction != null ? prefHeightFunction.apply(resizable) : 0.0;
+	}
+
+	/**
+	 * Computes the maximum width for the specified {@link MFXResizable} by invoking the set {@link #getMaxWidthFunction()}.
+	 *
+	 * @return the computed width or 0.0 if the function is null
+	 */
+	public double computeMaxWidth(MFXResizable resizable) {
+		return maxWidthFunction != null ? maxWidthFunction.apply(resizable) : 0.0;
+	}
+
+	/**
+	 * Computes the maximum height for the specified {@link MFXResizable} by invoking the set {@link #getMaxHeightFunction()}.
+	 *
+	 * @return the computed height or 0.0 if the function is null
+	 */
+	public double computeMaxHeight(MFXResizable resizable) {
+		return maxHeightFunction != null ? maxHeightFunction.apply(resizable) : 0.0;
+	}
+
+	/**
+	 * Fluent API to set this {@code LayoutStrategy} on the given {@link MFXResizable}.
+	 *
+	 * @see MFXResizable#setLayoutStrategy(LayoutStrategy)
+	 */
+	public LayoutStrategy setOn(MFXResizable resizable) {
+		resizable.setLayoutStrategy(this);
+		return this;
+	}
+
+	//================================================================================
+	// Overridden Methods
+	//================================================================================
+	@Override
+	public boolean equals(Object o) {
+		if (this == o) return true;
+		if (o == null || getClass() != o.getClass()) return false;
+		LayoutStrategy that = (LayoutStrategy) o;
+		return Objects.equals(getMinWidthFunction(), that.getMinWidthFunction()) &&
+				Objects.equals(getMinHeightFunction(), that.getMinHeightFunction()) &&
+				Objects.equals(getPrefWidthFunction(), that.getPrefWidthFunction()) &&
+				Objects.equals(getPrefHeightFunction(), that.getPrefHeightFunction()) &&
+				Objects.equals(getMaxWidthFunction(), that.getMaxWidthFunction()) &&
+				Objects.equals(getMaxHeightFunction(), that.getMaxHeightFunction());
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(
+				getMinWidthFunction(), getMinHeightFunction(),
+				getPrefWidthFunction(), getPrefHeightFunction(),
+				getMaxWidthFunction(), getMaxHeightFunction()
+		);
+	}
+
+	//================================================================================
+	// Getters/Setters
+	//================================================================================
+	public Function<MFXResizable, Double> getMinWidthFunction() {
+		return minWidthFunction;
+	}
+
+	public LayoutStrategy setMinWidthFunction(Function<MFXResizable, Double> minWidthFunction) {
+		this.minWidthFunction = minWidthFunction;
+		return this;
+	}
+
+	public Function<MFXResizable, Double> getMinHeightFunction() {
+		return minHeightFunction;
+	}
+
+	public LayoutStrategy setMinHeightFunction(Function<MFXResizable, Double> minHeightFunction) {
+		this.minHeightFunction = minHeightFunction;
+		return this;
+	}
+
+	public Function<MFXResizable, Double> getPrefWidthFunction() {
+		return prefWidthFunction;
+	}
+
+	public LayoutStrategy setPrefWidthFunction(Function<MFXResizable, Double> prefWidthFunction) {
+		this.prefWidthFunction = prefWidthFunction;
+		return this;
+	}
+
+	public Function<MFXResizable, Double> getPrefHeightFunction() {
+		return prefHeightFunction;
+	}
+
+	public LayoutStrategy setPrefHeightFunction(Function<MFXResizable, Double> prefHeightFunction) {
+		this.prefHeightFunction = prefHeightFunction;
+		return this;
+	}
+
+	public Function<MFXResizable, Double> getMaxWidthFunction() {
+		return maxWidthFunction;
+	}
+
+	public LayoutStrategy setMaxWidthFunction(Function<MFXResizable, Double> maxWidthFunction) {
+		this.maxWidthFunction = maxWidthFunction;
+		return this;
+	}
+
+	public Function<MFXResizable, Double> getMaxHeightFunction() {
+		return maxHeightFunction;
+	}
+
+	public LayoutStrategy setMaxHeightFunction(Function<MFXResizable, Double> maxHeightFunction) {
+		this.maxHeightFunction = maxHeightFunction;
+		return this;
+	}
+
+	public static class Defaults {
+		public static final Function<MFXResizable, Double> DEF_MIN_WIDTH_FUNCTION;
+		public static final Function<MFXResizable, Double> DEF_MIN_HEIGHT_FUNCTION;
+		public static final Function<MFXResizable, Double> DEF_PREF_WIDTH_FUNCTION;
+		public static final Function<MFXResizable, Double> DEF_PREF_HEIGHT_FUNCTION;
+		public static final Function<MFXResizable, Double> DEF_MAX_WIDTH_FUNCTION;
+		public static final Function<MFXResizable, Double> DEF_MAX_HEIGHT_FUNCTION;
+
+		static {
+			DEF_MIN_WIDTH_FUNCTION = createFunction(
+					MFXResizable::getHeight,
+					(s, h, i) -> s.computeMinWidth(h, i.getTop(), i.getRight(), i.getBottom(), i.getLeft())
+			);
+			DEF_MIN_HEIGHT_FUNCTION = createFunction(
+					MFXResizable::getWidth,
+					(s, w, i) -> s.computeMinHeight(w, i.getTop(), i.getRight(), i.getBottom(), i.getLeft())
+			);
+			DEF_PREF_WIDTH_FUNCTION = createFunction(
+					MFXResizable::getHeight,
+					(s, h, i) -> s.computePrefWidth(h, i.getTop(), i.getRight(), i.getBottom(), i.getLeft())
+			);
+			DEF_PREF_HEIGHT_FUNCTION = createFunction(
+					MFXResizable::getWidth,
+					(s, w, i) -> s.computePrefHeight(w, i.getTop(), i.getRight(), i.getBottom(), i.getLeft())
+			);
+			DEF_MAX_WIDTH_FUNCTION = createFunction(
+					MFXResizable::getHeight,
+					(s, h, i) -> s.computeMaxWidth(h, i.getTop(), i.getRight(), i.getBottom(), i.getLeft())
+			);
+			DEF_MAX_HEIGHT_FUNCTION = createFunction(
+					MFXResizable::getWidth,
+					(s, w, i) -> s.computeMaxHeight(w, i.getTop(), i.getRight(), i.getBottom(), i.getLeft())
+			);
+		}
+
+		private static Function<MFXResizable, Double> createFunction(
+				Function<MFXResizable, Double> otherSize,
+				TriFunction<MFXSkinBase<?, ?>, Double, Insets, Double> fn) {
+			return c -> {
+				Skin<?> skin = c.getSkin();
+				if (!(skin instanceof MFXSkinBase)) return 0.0;
+				Double oSize = otherSize.apply(c);
+				return fn.apply(((MFXSkinBase<?, ?>) skin), oSize, getSnappedInsets(c));
+			};
+		}
+
+		private static Insets getSnappedInsets(MFXResizable res) {
+			return InsetsBuilder.of(
+					res.snappedTopInset(),
+					res.snappedRightInset(),
+					res.snappedBottomInset(),
+					res.snappedLeftInset()
+			);
+		}
+	}
+}

+ 117 - 0
modules/components/src/main/java/io/github/palexdev/mfxcomponents/layout/MFXResizable.java

@@ -0,0 +1,117 @@
+/*
+ * 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.layout;
+
+import io.github.palexdev.mfxcomponents.controls.base.MFXControl;
+import io.github.palexdev.mfxcomponents.controls.base.MFXLabeled;
+import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
+import javafx.beans.property.ObjectProperty;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+
+/**
+ * This API allows MaterialFX components, descendants of {@link MFXControl} and {@link MFXLabeled}, which also use the
+ * new base skin {@link MFXSkinBase}, to define a quick and easy way to change their layout strategy through a property.
+ * <p>
+ * The pro of such API is that a user doesn't have to necessarily create a custom skin or override the methods inline
+ * to change the component sizing, it's enough to define a new strategy, which is more elegant indeed.
+ * <p></p>
+ * Some methods present in this API are already defined by JavaFX controls, but forces them to be {@code public} since
+ * they may be needed by the {@link LayoutStrategy} when computing the sizes.
+ */
+public interface MFXResizable {
+
+	/**
+	 * @return the instance of the current {@link LayoutStrategy}
+	 */
+	LayoutStrategy getLayoutStrategy();
+
+	/**
+	 * Specifies the {@link LayoutStrategy} used by the component to compute its sizes.
+	 */
+	ObjectProperty<LayoutStrategy> layoutStrategyProperty();
+
+	/**
+	 * Sets the {@link LayoutStrategy} used by the component to compute its sizes.
+	 */
+	void setLayoutStrategy(LayoutStrategy strategy);
+
+	/**
+	 * By default, does nothing.
+	 * <p>
+	 * Implementations of this should perform the actions needed to 'activate' the new layout strategy,
+	 * for example a component may invoke a layout request through {@link Control#requestLayout()}.
+	 */
+	default void onLayoutStrategyChanged() {
+	}
+
+	/**
+	 * By defaults, returns {@link LayoutStrategy#defaultStrategy()}.
+	 * <p></p>
+	 * Components may override this to specify what is their default layout strategy.
+	 */
+	default LayoutStrategy defaultLayoutStrategy() {
+		return LayoutStrategy.defaultStrategy();
+	}
+
+	/**
+	 * Calls {@link #setLayoutStrategy(LayoutStrategy)} with {@link #defaultLayoutStrategy()} as parameter.
+	 * In other words, resets the component's {@link LayoutStrategy} to its default one.
+	 */
+	default void setDefaultLayoutStrategy() {
+		setLayoutStrategy(defaultLayoutStrategy());
+	}
+
+	/**
+	 * Calls {@link #setLayoutStrategy(LayoutStrategy)} with {@link LayoutStrategy#defaultStrategy()} as parameter.
+	 * In other words, resets the component's {@link LayoutStrategy} to the one used by JavaFX.
+	 * <p>
+	 * This may be useful in case {@link #defaultLayoutStrategy()} has been overridden.
+	 */
+	default void setJavaFXLayoutStrategy() {
+		setLayoutStrategy(LayoutStrategy.defaultStrategy());
+	}
+
+	double getWidth();
+
+	double getHeight();
+
+	Skin<?> getSkin();
+
+	double snappedTopInset();
+
+	double snappedRightInset();
+
+	double snappedBottomInset();
+
+	double snappedLeftInset();
+
+	double computeMinWidth(double height);
+
+	double computeMinHeight(double width);
+
+	double computePrefWidth(double height);
+
+	double computePrefHeight(double width);
+
+	double computeMaxWidth(double height);
+
+	double computeMaxHeight(double width);
+
+}

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

@@ -112,7 +112,7 @@ public class MFXButtonSkin extends MFXSkinBase<MFXButton, MFXButtonBehavior> {
 	}
 
 	@Override
-	protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+	public double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 		MFXButton button = getSkinnable();
 		double insets = leftInset + rightInset;
 		double tW = TextUtils.computeTextWidth(label.getFont(), label.getText());
@@ -122,7 +122,7 @@ public class MFXButtonSkin extends MFXSkinBase<MFXButton, MFXButtonBehavior> {
 	}
 
 	@Override
-	protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+	public double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 		MFXButton button = getSkinnable();
 		double insets = topInset + bottomInset;
 		double tH = TextUtils.computeTextHeight(label.getFont(), label.getText());
@@ -131,12 +131,12 @@ public class MFXButtonSkin extends MFXSkinBase<MFXButton, MFXButtonBehavior> {
 	}
 
 	@Override
-	protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+	public double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 		return getSkinnable().prefWidth(height);
 	}
 
 	@Override
-	protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+	public double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 		return getSkinnable().prefHeight(width);
 	}
 

+ 3 - 4
modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXFabSkin.java

@@ -80,19 +80,18 @@ public class MFXFabSkin extends MFXButtonSkin {
 	}
 
 	@Override
-	protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+	public 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) {
+	public 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() ?
+		return fab.isExtended() ?
 				leftInset + iW + TextUtils.computeTextWidth(fab.getFont(), fab.getText()) + rightInset :
 				leftInset + iW + rightInset;
-		return Math.max(w, fab.getInitWidth());
 	}
 
 	@Override

+ 3 - 0
modules/components/src/main/java/module-info.java

@@ -14,6 +14,9 @@ module mfx.components {
 	exports io.github.palexdev.mfxcomponents.controls.buttons;
 	exports io.github.palexdev.mfxcomponents.controls.fab;
 
+	// Layout
+	exports io.github.palexdev.mfxcomponents.layout;
+
 	// Skins
 	exports io.github.palexdev.mfxcomponents.skins;
 

+ 2 - 2
modules/components/src/test/java/interactive/TestInitSize.java

@@ -22,7 +22,7 @@ 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 io.github.palexdev.mfxcomponents.theming.enums.MFXThemeManager;
 import javafx.scene.Scene;
 import javafx.scene.layout.StackPane;
 import javafx.stage.Stage;
@@ -114,7 +114,7 @@ public class TestInitSize {
 	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
+			MFXThemeManager.LIGHT.addOn(scene);
 			FxToolkit.setupStage(s -> s.setScene(scene));
 		} catch (TimeoutException e) {
 			throw new RuntimeException(e);

+ 450 - 0
modules/components/src/test/java/interactive/TestLayoutStrategies.java

@@ -0,0 +1,450 @@
+/*
+ * 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.base.MFXSkinBase;
+import io.github.palexdev.mfxcomponents.controls.fab.MFXFab;
+import io.github.palexdev.mfxcomponents.layout.LayoutStrategy;
+import io.github.palexdev.mfxcomponents.layout.LayoutStrategy.Defaults;
+import io.github.palexdev.mfxcomponents.layout.MFXResizable;
+import io.github.palexdev.mfxcomponents.theming.enums.MFXThemeManager;
+import io.github.palexdev.mfxcore.base.properties.NodeProperty;
+import io.github.palexdev.mfxcore.behavior.BehaviorBase;
+import io.github.palexdev.mfxcore.observables.When;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+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.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;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+@ExtendWith(ApplicationExtension.class)
+public class TestLayoutStrategies {
+	private static Stage stage;
+
+	@Start
+	void start(Stage stage) {
+		TestLayoutStrategies.stage = stage;
+		stage.show();
+	}
+
+	@Test
+	void testDefaultStrategy(FxRobot robot) {
+		StackPane root = setupStage();
+		ResControl rc = new ResControl();
+		robot.interact(() -> root.getChildren().setAll(rc));
+
+		assertEquals(root.getWidth(), rc.getWidth());
+		assertEquals(root.getHeight(), rc.getHeight());
+	}
+
+	@Test
+	void testStrategyMin(FxRobot robot) {
+		StackPane root = setupStage();
+		ResControl rc = new ResControl();
+		robot.interact(() -> root.getChildren().setAll(rc));
+
+		// Exactly as above because in a StackPane...
+		// Let's set the max to use the pref...
+		assertEquals(root.getWidth(), rc.getWidth());
+		assertEquals(root.getHeight(), rc.getHeight());
+
+		robot.interact(() -> rc.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE));
+		assertEquals(0, Math.abs(rc.getWidth()));
+		assertEquals(0, Math.abs(rc.getWidth()));
+		// 0 because there's no content
+
+		robot.interact(() -> rc.setContent(new MFXFontIcon("fas-circle", 24)));
+		assertEquals(24, rc.getWidth());
+		assertEquals(24, rc.getHeight());
+
+		// Finally, let's test the strategy
+		LayoutStrategy strategy = LayoutStrategy.defaultStrategy()
+				.setMinWidthFunction(Defaults.DEF_MIN_WIDTH_FUNCTION.andThen(r -> Math.max(r, 48)))
+				.setMinHeightFunction(Defaults.DEF_MIN_HEIGHT_FUNCTION.andThen(r -> Math.max(r, 48)));
+		robot.interact(() -> {
+			rc.setLayoutStrategy(strategy);
+			rc.requestLayout();
+		});
+		assertEquals(48, rc.getWidth());
+		assertEquals(48, rc.getHeight());
+	}
+
+	@Test
+	void testStrategyPref(FxRobot robot) {
+		StackPane root = setupStage();
+		ResControl rc = new ResControl();
+		rc.setContent(new MFXFontIcon("fas-circle", 24));
+		rc.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+		robot.interact(() -> root.getChildren().setAll(rc));
+
+		assertEquals(24.0, rc.getWidth());
+		assertEquals(24.0, rc.getHeight());
+
+		LayoutStrategy strategy = LayoutStrategy.defaultStrategy()
+				.setPrefWidthFunction(Defaults.DEF_PREF_WIDTH_FUNCTION.andThen(r -> Math.max(r, 64.0)))
+				.setPrefHeightFunction(Defaults.DEF_PREF_HEIGHT_FUNCTION.andThen(r -> Math.max(r, 64.0)));
+		robot.interact(() -> {
+			rc.setLayoutStrategy(strategy);
+			rc.requestLayout();
+		});
+
+		assertEquals(64.0, rc.getWidth());
+		assertEquals(64.0, rc.getHeight());
+
+		robot.interact(() -> rc.setMaxSize(30, 30));
+		assertEquals(30.0, rc.getWidth());
+		assertEquals(30.0, rc.getHeight());
+
+		robot.interact(() -> rc.setMinSize(45, 45));
+		assertEquals(45.0, rc.getWidth());
+		assertEquals(45.0, rc.getHeight());
+	}
+
+	@Test
+	void testStrategyMax(FxRobot robot) {
+		StackPane root = setupStage();
+		ResControl rc = new ResControl();
+		rc.setContent(new MFXFontIcon("fas-circle", 40.0));
+		robot.interact(() -> root.getChildren().setAll(rc));
+
+		LayoutStrategy strategy = LayoutStrategy.defaultStrategy()
+				.setMaxWidthFunction(Defaults.DEF_MAX_WIDTH_FUNCTION.andThen(r -> Math.min(r, 50.0)))
+				.setMaxHeightFunction(Defaults.DEF_MAX_HEIGHT_FUNCTION.andThen(r -> Math.min(r, 50.0)));
+		robot.interact(() -> {
+			rc.setLayoutStrategy(strategy);
+			rc.requestLayout();
+		});
+
+		// Notice the difference between the other tests, the strategy is using Math.min
+		assertEquals(50.0, rc.getWidth());
+		assertEquals(50.0, rc.getHeight());
+
+		robot.interact(() -> rc.setPrefSize(100, 100));
+		assertEquals(50.0, rc.getWidth());
+		assertEquals(50.0, rc.getHeight());
+
+		// Remember, min sizes always prevail on max
+		robot.interact(() -> rc.setMinSize(70, 70));
+		assertEquals(70.0, rc.getWidth());
+		assertEquals(70.0, rc.getHeight());
+	}
+
+	@Test
+	void testFABDefaultStrategy(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = MFXFab.extended();
+		fab.setIcon(FONTAWESOME_SOLID.randomIcon());
+		robot.interact(() -> {
+			fab.setJavaFXLayoutStrategy();
+			root.getChildren().setAll(fab);
+		});
+
+		// Default strategy, same as JavaFX
+		// Do not count insets for the sanity of the below 'comparison'
+		assertEquals(fab.getIcon().getLayoutBounds().getWidth(), fab.getWidth() - getLRInsets(fab));
+		assertEquals(fab.getIcon().getLayoutBounds().getHeight(), fab.getHeight() - getTBInsets(fab));
+
+		// Double check with a similar container setup...
+		StackPane sp = new StackPane(new MFXFontIcon(fab.getIcon().getDescription(), fab.getIcon().getSize()));
+		sp.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+		robot.interact(() -> root.getChildren().add(sp));
+		assertEquals(fab.getIcon().getLayoutBounds().getWidth(), sp.getWidth());
+		assertEquals(fab.getIcon().getLayoutBounds().getHeight(), sp.getHeight());
+	}
+
+	@Test
+	void testFABStrategyMin(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = MFXFab.extended();
+		fab.setIcon(new MFXFontIcon("fas-circle"));
+		robot.interact(() -> root.getChildren().setAll(fab));
+
+		// Define a strategy with minimum sizes
+		LayoutStrategy strategy = LayoutStrategy.defaultStrategy()
+				.setMinWidthFunction(Defaults.DEF_MIN_WIDTH_FUNCTION.andThen(r -> Math.max(r, 64.0)))
+				.setMinHeightFunction(Defaults.DEF_MIN_HEIGHT_FUNCTION.andThen(r -> Math.max(r, 64.0)));
+		robot.interact(() -> fab.setLayoutStrategy(strategy));
+
+		assertEquals(64.0, fab.getWidth());
+		assertEquals(64.0, fab.getHeight());
+
+		// What happens if I set the pref?
+		robot.interact(() -> fab.setPrefSize(100, 100));
+		assertEquals(100.0, fab.getWidth());
+		assertEquals(100.0, fab.getHeight());
+
+		// What happens if I also set the max?
+		robot.interact(() -> fab.setMaxSize(40, 40));
+		assertEquals(64.0, fab.getWidth());
+		assertEquals(64.0, fab.getHeight());
+		// Still 64, does this happen with JavaFX nodes too?
+
+		StackPane sp = new StackPane() {
+			@Override
+			protected double computeMinWidth(double height) {
+				return Math.max(super.computeMinWidth(height), 64.0);
+			}
+
+			@Override
+			protected double computeMinHeight(double width) {
+				return Math.max(super.computeMinHeight(width), 64.0);
+			}
+		};
+		sp.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+		robot.interact(() -> root.getChildren().setAll(sp));
+
+		assertEquals(64.0, sp.getWidth());
+		assertEquals(64.0, sp.getHeight());
+
+		robot.interact(() -> sp.setPrefSize(100, 100));
+		assertEquals(100.0, sp.getWidth());
+		assertEquals(100.0, sp.getHeight());
+
+		robot.interact(() -> sp.setMaxSize(40, 40));
+		assertEquals(64.0, sp.getWidth());
+		assertEquals(64.0, sp.getHeight());
+
+		/*
+		 * Yes, so, let me get this straight. Layout:
+		 * Min > Pref > Max
+		 * Which means that even if Max is lesser than Min, the latter will prevail
+		 * But first the minimum between pref and max is computed
+		 *
+		 * The formula seems to be:
+		 * 1) Maximum between Pref and Min
+		 * 2) Maximum between Min and Max
+		 * 3) Minimum between these two results
+		 */
+	}
+
+	@Test
+	void testFABStrategyPref(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = MFXFab.extended();
+		fab.setIcon(new MFXFontIcon("fas-circle"));
+		robot.interact(() -> root.getChildren().setAll(fab));
+
+		/*
+		 * At this point the layout strategy is still not set and the extend() method
+		 * in the FAB behavior is called causing the pref width to be overridden through
+		 * the setPrefWidth method
+		 */
+		assertNotEquals(64.0, fab.getWidth());
+
+		// Define a strategy with pref sizes
+		LayoutStrategy strategy = LayoutStrategy.defaultStrategy()
+				.setPrefWidthFunction(Defaults.DEF_PREF_WIDTH_FUNCTION.andThen(r -> Math.max(r, 64.0)))
+				.setPrefHeightFunction(Defaults.DEF_PREF_HEIGHT_FUNCTION.andThen(r -> Math.max(r, 64.0)));
+		robot.interact(() -> fab.setLayoutStrategy(strategy));
+
+		/*
+		 * This is different from using setPrefSize(...)!
+		 * And different from setting a minimum size strategy, so...
+		 */
+		assertEquals(64.0, fab.getWidth());
+		assertEquals(64.0, fab.getHeight());
+
+		// What happens if I set a max?
+		robot.interact(() -> fab.setMaxSize(30, 30));
+		assertEquals(30.0, fab.getWidth());
+		assertEquals(30.0, fab.getHeight());
+
+		// What happens if I set a min?
+		robot.interact(() -> fab.setMinSize(45, 45));
+		assertEquals(45.0, fab.getWidth());
+		assertEquals(45.0, fab.getHeight());
+	}
+
+	@Test
+	void testFABStrategyMax(FxRobot robot) {
+		StackPane root = setupStage();
+		MFXFab fab = MFXFab.extended();
+		fab.setIcon(new MFXFontIcon("fas-circle"));
+		robot.interact(() -> root.getChildren().setAll(fab));
+
+		// Define a strategy with max sizes
+		LayoutStrategy strategy = LayoutStrategy.defaultStrategy()
+				.setMaxWidthFunction(Defaults.DEF_MAX_WIDTH_FUNCTION.andThen(r -> Math.max(r, 64.0)))
+				.setMaxHeightFunction(Defaults.DEF_MAX_HEIGHT_FUNCTION.andThen(r -> Math.max(r, 64.0)));
+		robot.interact(() -> fab.setLayoutStrategy(strategy));
+
+		/*
+		 * Here we have a special case. We set a strategy that constraints the node to be at max 64px
+		 * But remember that the node is inside a StackPane so it will occupy all the space possible within its constraints
+		 * So we have to checks to do: 1) the pref size 2) the actual size
+		 *
+		 * We don't check for the prefHeight because only the pref width is overridden by the
+		 * extend() method in the FAB behavior
+		 */
+		assertEquals(60.0, fab.getPrefWidth());
+		assertEquals(64.0, fab.getWidth());
+		assertEquals(64.0, fab.getHeight());
+
+		// What happens if I set pref?
+		robot.interact(() -> fab.setPrefSize(100, 100));
+		assertEquals(100.0, fab.getWidth());
+		assertEquals(100.0, fab.getHeight());
+		/*
+		 * Why 100?
+		 * Buttons by default use the prefSize for the computation of the maxSize methods
+		 * This means that the above layout strategy is ignored because at the end of the
+		 * algorithm the Math.min operation is evaluated between the same value (comes from pref size)
+		 * Let's see if the strategy changes what happens...
+		 */
+
+		LayoutStrategy newStrategy = LayoutStrategy.defaultStrategy()
+				.setMaxWidthFunction(r -> 64.0)
+				.setMaxHeightFunction(r -> 64.0);
+		robot.interact(() -> fab.setLayoutStrategy(newStrategy));
+		assertEquals(64.0, fab.getWidth());
+		assertEquals(64.0, fab.getHeight());
+		// Exactly as expected...
+	}
+
+	private double getLRInsets(Region region) {
+		return region.snappedLeftInset() + region.snappedRightInset();
+	}
+
+	private double getTBInsets(Region region) {
+		return region.snappedTopInset() + region.snappedBottomInset();
+	}
+
+	private StackPane setupStage() {
+		try {
+			Scene scene = new Scene(new StackPane(), 200, 200);
+			MFXThemeManager.LIGHT.addOn(scene);
+			FxToolkit.setupStage(s -> s.setScene(scene));
+		} catch (TimeoutException e) {
+			throw new RuntimeException(e);
+		}
+		return (StackPane) stage.getScene().getRoot();
+	}
+
+	private static class ResControl extends Control implements MFXResizable {
+		private final ObjectProperty<LayoutStrategy> layoutStrategy = new SimpleObjectProperty<>(defaultLayoutStrategy());
+		private final NodeProperty content = new NodeProperty();
+
+		@Override
+		public LayoutStrategy getLayoutStrategy() {
+			return layoutStrategy.get();
+		}
+
+		@Override
+		public ObjectProperty<LayoutStrategy> layoutStrategyProperty() {
+			return layoutStrategy;
+		}
+
+		@Override
+		public void setLayoutStrategy(LayoutStrategy strategy) {
+			layoutStrategy.set(strategy);
+		}
+
+		@Override
+		public double computeMinWidth(double height) {
+			return getLayoutStrategy().computeMinWidth(this);
+		}
+
+		@Override
+		public double computeMinHeight(double width) {
+			return getLayoutStrategy().computeMinWidth(this);
+		}
+
+		@Override
+		public double computePrefWidth(double height) {
+			return getLayoutStrategy().computePrefWidth(this);
+		}
+
+		@Override
+		public double computePrefHeight(double width) {
+			return getLayoutStrategy().computePrefHeight(this);
+		}
+
+		@Override
+		public double computeMaxWidth(double height) {
+			return getLayoutStrategy().computeMaxWidth(this);
+		}
+
+		@Override
+		public double computeMaxHeight(double width) {
+			return getLayoutStrategy().computeMaxHeight(this);
+		}
+
+		public Node getContent() {
+			return content.get();
+		}
+
+		public NodeProperty contentProperty() {
+			return content;
+		}
+
+		public void setContent(Node content) {
+			this.content.set(content);
+		}
+
+		@SuppressWarnings({"rawtypes", "unchecked"})
+		@Override
+		protected Skin<?> createDefaultSkin() {
+			return new MFXSkinBase(this) {
+				@Override
+				protected void initBehavior(BehaviorBase behavior) {
+				}
+
+				final Pane pane = new Pane();
+
+				{
+					When.onChanged(contentProperty())
+							.then((o, n) -> {
+								if (n == null) {
+									pane.getChildren().clear();
+									return;
+								}
+								pane.getChildren().setAll(n);
+							})
+							.executeNow()
+							.listen();
+					getChildren().add(pane);
+				}
+
+				@Override
+				public void dispose() {
+					When.disposeFor(contentProperty());
+					super.dispose();
+				}
+			};
+		}
+	}
+}