浏览代码

Revert: MFXListView is now a legacy component (MFXLegacyListView)

Flowless and the dependency ReactFX are old unmaintained projects that may contain bugs. Also to use Flowless-based controls the user must add ReactFX to SceneBuilder. To mitigate all these issues the current list view implementation will still be considered the default one, for those who want to try something else or have issues/bugs with JavaFX's list view you can try MFXFlowlessListView.

Signed-off-by: PAlex404 <alessandro.parisi406@gmail.com>
PAlex404 4 年之前
父节点
当前提交
1b22d28298
共有 52 个文件被更改,包括 5163 次插入166 次删除
  1. 21 21
      .run/MaterialFX [run].run.xml
  2. 1 1
      demo/build.gradle
  3. 145 39
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ListViewDemoController.java
  4. 1 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/listviews_demo.css
  5. 43 26
      demo/src/main/resources/io/github/palexdev/materialfx/demo/listviews_demo.fxml
  6. 3 3
      materialfx/build.gradle
  7. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckTreeItem.java
  8. 308 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFlowlessListView.java
  9. 16 16
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListView.java
  10. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java
  11. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTreeItem.java
  12. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTreeView.java
  13. 110 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXFlowlessListCell.java
  14. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXTreeItem.java
  15. 92 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXFlowlessListCell.java
  16. 19 19
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXListCell.java
  17. 133 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/Cell.java
  18. 163 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellListManager.java
  19. 74 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellPool.java
  20. 245 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellPositioner.java
  21. 164 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellWrapper.java
  22. 87 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/MFXVirtualizedScrollPane.java
  23. 423 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/Navigator.java
  24. 475 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/OrientationHelper.java
  25. 144 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/ScaledVirtualized.java
  26. 222 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/SizeTracker.java
  27. 102 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/StableBidirectionalVar.java
  28. 263 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/TargetPosition.java
  29. 646 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/VirtualFlow.java
  30. 227 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/VirtualFlowHit.java
  31. 119 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/Virtualized.java
  32. 383 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/VirtualizedScrollPane.java
  33. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java
  34. 0 17
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/ITableSelectionModel.java
  35. 116 0
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/ListSelectionModel.java
  36. 1 0
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/TableSelectionModel.java
  37. 1 0
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/TreeCheckModel.java
  38. 1 0
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/TreeSelectionModel.java
  39. 32 0
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/IListSelectionModel.java
  40. 35 0
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITableSelectionModel.java
  41. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITreeCheckModel.java
  42. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITreeSelectionModel.java
  43. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java
  44. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java
  45. 170 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFlowlessListViewSkin.java
  46. 7 7
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXListViewSkin.java
  47. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java
  48. 4 1
      materialfx/src/main/java/module-info.java
  49. 37 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-flowless-listcell.css
  50. 114 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-flowless-listview.css
  51. 0 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listcell.css
  52. 0 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listview.css

+ 21 - 21
.run/MaterialFX [run].run.xml

@@ -1,23 +1,23 @@
 <component name="ProjectRunConfigurationManager">
-    <configuration default="false" name="MaterialFX [run]" type="GradleRunConfiguration" factoryName="Gradle">
-        <ExternalSystemSettings>
-            <option name="executionName"/>
-            <option name="externalProjectPath" value="$PROJECT_DIR$"/>
-            <option name="externalSystemIdString" value="GRADLE"/>
-            <option name="scriptParameters" value="-PchooseMain=io.github.palexdev.materialfx.demo.Demo"/>
-            <option name="taskDescriptions">
-                <list/>
-            </option>
-            <option name="taskNames">
-                <list>
-                    <option value="run"/>
-                </list>
-            </option>
-            <option name="vmOptions" value=""/>
-        </ExternalSystemSettings>
-        <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
-        <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-        <DebugAllEnabled>false</DebugAllEnabled>
-        <method v="2"/>
-    </configuration>
+  <configuration default="false" name="MaterialFX [run]" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="-PchooseMain=io.github.palexdev.materialfx.demo.Demo --stacktrace" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="run" />
+        </list>
+      </option>
+      <option name="vmOptions" value="" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <method v="2" />
+  </configuration>
 </component>

+ 1 - 1
demo/build.gradle

@@ -39,4 +39,4 @@ jlink {
         imageOptions = ['--icon', 'src/main/resources/logo.ico']
     }
     forceMerge('log4j-api')
-}
+}

+ 145 - 39
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/ListViewDemoController.java

@@ -1,9 +1,13 @@
 package io.github.palexdev.materialfx.demo.controllers;
 
 import io.github.palexdev.materialfx.controls.MFXButton;
-import io.github.palexdev.materialfx.controls.legacy.MFXLegacyListView;
+import io.github.palexdev.materialfx.controls.MFXFlowlessListView;
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.MFXListView;
 import io.github.palexdev.materialfx.effects.DepthLevel;
 import io.github.palexdev.materialfx.utils.ColorUtils;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 import javafx.fxml.FXML;
@@ -23,17 +27,44 @@ import java.util.ResourceBundle;
 public class ListViewDemoController implements Initializable {
     private final Random random = new Random(System.currentTimeMillis());
 
+    private enum State {
+        LEGACY, NEW
+    }
+
+    private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.NEW);
+
+    @FXML
+    private HBox legacyBox;
+
+    @FXML
+    private HBox newBox;
+
+    @FXML
+    private MFXListView<String> stringView;
+
+    @FXML
+    private MFXListView<MFXLabel> labelView;
+
+    @FXML
+    private MFXListView<HBox> hBoxView;
+
+    @FXML
+    private MFXListView<String> cssView;
+
+    @FXML
+    private MFXFlowlessListView<String> stringViewNew;
+
     @FXML
-    private MFXLegacyListView<String> stringView;
+    private MFXFlowlessListView<MFXLabel> labelViewNew;
 
     @FXML
-    private MFXLegacyListView<Label> labelView;
+    private MFXFlowlessListView<HBox> hBoxViewNew;
 
     @FXML
-    private MFXLegacyListView<HBox> hBoxView;
+    private MFXFlowlessListView<String> cssViewNew;
 
     @FXML
-    private MFXLegacyListView<String> cssView;
+    private MFXButton switchButton;
 
     @FXML
     private MFXButton depthButton;
@@ -41,9 +72,46 @@ public class ListViewDemoController implements Initializable {
     @FXML
     private MFXButton colorsButton;
 
+    private ObservableList<String> stringList;
+    private ObservableList<MFXLabel> labelsList;
+    private ObservableList<HBox> hBoxesList;
+
+    private ObservableList<MFXLabel> labelsListNew;
+    private ObservableList<HBox> hBoxesListNew;
+
     @Override
     public void initialize(URL location, ResourceBundle resources) {
-        ObservableList<String> stringList = FXCollections.observableArrayList(List.of(
+        initLists();
+
+        state.addListener((observable, oldValue, newValue) -> {
+            if (newValue == State.NEW) {
+                legacyBox.setVisible(false);
+                newBox.setVisible(true);
+            } else {
+                legacyBox.setVisible(true);
+                newBox.setVisible(false);
+            }
+        });
+
+        //  LEGACY //
+        stringView.setItems(stringList);
+        labelView.setItems(labelsList);
+        hBoxView.setItems(hBoxesList);
+        cssView.setItems(stringList);
+
+        // NEW //
+        stringViewNew.setItems(stringList);
+        labelViewNew.setItems(labelsListNew);
+        hBoxViewNew.setItems(hBoxesListNew);
+        cssViewNew.setItems(stringList);
+
+        switchButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> updateState());
+        depthButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> updateDepth());
+        colorsButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> updateColors());
+    }
+
+    private void initLists() {
+        stringList = FXCollections.observableArrayList(List.of(
                 "String 0",
                 "String 1",
                 "String 2",
@@ -53,21 +121,19 @@ public class ListViewDemoController implements Initializable {
                 "String 6",
                 "String 7"
         ));
-        stringView.setItems(stringList);
 
-        ObservableList<Label> labelsList = FXCollections.observableArrayList(List.of(
-                new Label("Label 0", createIcon("fas-home")),
-                new Label("Label 1", createIcon("fas-star")),
-                new Label("Label 2", createIcon("fas-heart")),
-                new Label("Label 3", createIcon("fas-cocktail")),
-                new Label("Label 4", createIcon("fas-anchor")),
-                new Label("Label 5", createIcon("fas-bolt")),
-                new Label("Label 6", createIcon("fas-bug")),
-                new Label("Label 7", createIcon("fas-beer"))
+        // LEGACY //
+        labelsList = FXCollections.observableArrayList(List.of(
+                createLabel("Label 0", "fas-home"),
+                createLabel("Label 1", "fas-star"),
+                createLabel("Label 2", "fas-heart"),
+                createLabel("Label 3", "fas-cocktail"),
+                createLabel("Label 4", "fas-anchor"),
+                createLabel("Label 5", "fas-apple-alt"),
+                createLabel("Label 6", "fas-bug"),
+                createLabel("Label 7", "fas-beer")
         ));
-        labelView.setItems(labelsList);
-
-        ObservableList<HBox> hBoxesList = FXCollections.observableArrayList(List.of(
+        hBoxesList = FXCollections.observableArrayList(List.of(
                 createHBox(0),
                 createHBox(1),
                 createHBox(2),
@@ -77,35 +143,48 @@ public class ListViewDemoController implements Initializable {
                 createHBox(6),
                 createHBox(7)
         ));
-        hBoxView.setItems(hBoxesList);
 
-        cssView.setItems(stringList);
-        depthButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
-            DepthLevel level = cssView.getDepthLevel();
-            if (level.equals(DepthLevel.LEVEL0)) {
-                cssView.setDepthLevel(DepthLevel.LEVEL2);
-            } else {
-                cssView.setDepthLevel(DepthLevel.LEVEL0);
-            }
-        });
-        colorsButton.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
-            cssView.setTrackColor(ColorUtils.getRandomColor());
-            cssView.setThumbColor(ColorUtils.getRandomColor());
-            cssView.setThumbHoverColor(ColorUtils.getRandomColor());
-        });
+        // NEW //
+        labelsListNew = FXCollections.observableArrayList(List.of(
+                createLabel("Label 0", "fas-home"),
+                createLabel("Label 1", "fas-star"),
+                createLabel("Label 2", "fas-heart"),
+                createLabel("Label 3", "fas-cocktail"),
+                createLabel("Label 4", "fas-anchor"),
+                createLabel("Label 5", "fas-apple-alt"),
+                createLabel("Label 6", "fas-bug"),
+                createLabel("Label 7", "fas-beer")
+        ));
+        hBoxesListNew = FXCollections.observableArrayList(List.of(
+                createHBox(0),
+                createHBox(1),
+                createHBox(2),
+                createHBox(3),
+                createHBox(4),
+                createHBox(5),
+                createHBox(6),
+                createHBox(7)
+        ));
     }
 
-    private FontIcon createIcon(String s) {
-        FontIcon icon = new FontIcon(s);
+    private MFXLabel createLabel(String text, String iconDescription) {
+        FontIcon icon = new FontIcon(iconDescription);
         icon.setIconColor(Color.PURPLE);
-        icon.setIconSize(13);
-        return icon;
+        icon.setIconSize(14);
+
+        MFXLabel label = new MFXLabel(text);
+        label.setLineColor(Color.TRANSPARENT);
+        label.setUnfocusedLineColor(Color.TRANSPARENT);
+        label.setStyle("-fx-background-color: transparent");
+        label.setLeadingIcon(icon);
+        label.setGraphicTextGap(10);
+        return label;
     }
 
     private HBox createHBox(int index) {
         HBox hBox = new HBox(20);
         hBox.setPadding(new Insets(0, 10, 0, 10));
-        hBox.setPrefSize(150, 30);
+        hBox.setPrefSize(200, 30);
 
         FontIcon city = new FontIcon("fas-city");
         city.setIconColor(Color.GOLD);
@@ -121,4 +200,31 @@ public class ListViewDemoController implements Initializable {
         return hBox;
     }
 
+    private void updateState() {
+        State curr = state.get();
+        switchButton.setText(curr == State.LEGACY ?  "Switch to Legacy" : "Switch to New");
+        state.set(curr == State.LEGACY ? State.NEW : State.LEGACY);
+    }
+
+    private void updateDepth() {
+        if (state.get() == State.LEGACY) {
+            DepthLevel level = cssView.getDepthLevel();
+            cssView.setDepthLevel(level.equals(DepthLevel.LEVEL0) ? DepthLevel.LEVEL2 : DepthLevel.LEVEL0);
+        } else {
+            DepthLevel level = cssViewNew.getDepthLevel();
+            cssViewNew.setDepthLevel(level.equals(DepthLevel.LEVEL0) ? DepthLevel.LEVEL2 : DepthLevel.LEVEL0);
+        }
+    }
+
+    private void updateColors() {
+        if (state.get() == State.LEGACY) {
+            cssView.setTrackColor(ColorUtils.getRandomColor());
+            cssView.setThumbColor(ColorUtils.getRandomColor());
+            cssView.setThumbHoverColor(ColorUtils.getRandomColor());
+        } else {
+            cssViewNew.setTrackColor(ColorUtils.getRandomColor());
+            cssViewNew.setThumbColor(ColorUtils.getRandomColor());
+            cssViewNew.setThumbHoverColor(ColorUtils.getRandomColor());
+        }
+    }
 }

+ 1 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/listviews_demo.css

@@ -21,6 +21,7 @@
     -fx-border-color: #7F0FFF;
     -fx-border-radius: 5;
     -fx-border-width: 2;
+    -fx-min-height: 32;
 }
 
 #label .text {

+ 43 - 26
demo/src/main/resources/io/github/palexdev/materialfx/demo/listviews_demo.fxml

@@ -1,58 +1,75 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<?import io.github.palexdev.materialfx.controls.legacy.MFXLegacyListView?>
-<?import io.github.palexdev.materialfx.controls.MFXButton?>
-<?import javafx.geometry.Insets?>
+<?import io.github.palexdev.materialfx.controls.*?>
+<?import javafx.geometry.*?>
 <?import javafx.scene.control.*?>
 <?import javafx.scene.layout.*?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
-           prefWidth="730.0" stylesheets="@css/listviews_demo.css" xmlns="http://javafx.com/javafx/11.0.1"
-           xmlns:fx="http://javafx.com/fxml/1"
-           fx:controller="io.github.palexdev.materialfx.demo.controllers.ListViewDemoController">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0" prefWidth="730.0" stylesheets="@css/listviews_demo.css" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.ListViewDemoController">
     <padding>
-        <Insets left="20.0" right="20.0"/>
+        <Insets left="20.0" right="20.0" />
     </padding>
-    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="ListViews"
-           StackPane.alignment="TOP_CENTER">
+    <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="ListViews" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="20.0"/>
+            <Insets top="20.0" />
         </StackPane.margin>
     </Label>
-    <HBox alignment="TOP_CENTER" maxHeight="-Infinity" prefHeight="250.0" prefWidth="680.0" spacing="20.0">
+    <HBox fx:id="legacyBox" alignment="TOP_CENTER" maxHeight="-Infinity" prefHeight="250.0" prefWidth="680.0" spacing="20.0" visible="false">
         <StackPane.margin>
-            <Insets top="-20.0"/>
+            <Insets top="40.0" />
+        </StackPane.margin>
+        <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="130.0" spacing="10.0">
+            <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="Standard" />
+            <MFXListView fx:id="stringView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0" prefWidth="120.0" />
+        </VBox>
+        <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="160.0" spacing="10.0">
+            <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="Labels" />
+            <MFXListView fx:id="labelView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0" prefWidth="150.0" />
+        </VBox>
+        <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="265.0" spacing="10.0">
+            <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="HBoxes" />
+            <MFXListView fx:id="hBoxView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0" prefWidth="265.0" />
+        </VBox>
+        <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="135.0" spacing="10.0">
+            <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="125.0" text="Customized and CSS" />
+            <MFXListView id="customView" fx:id="cssView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0" prefWidth="110.0" stylesheets="@css/listviews_demo.css" />
+        </VBox>
+    </HBox>
+    <HBox fx:id="newBox" alignment="TOP_CENTER" maxHeight="-Infinity" prefHeight="250.0" prefWidth="680.0"
+          spacing="20.0">
+        <StackPane.margin>
+            <Insets top="40.0"/>
         </StackPane.margin>
         <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="130.0"
               spacing="10.0">
             <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="Standard"/>
-            <MFXLegacyListView fx:id="stringView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0"
-                               prefWidth="120.0"/>
+            <MFXFlowlessListView fx:id="stringViewNew"/>
         </VBox>
         <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="160.0"
               spacing="10.0">
             <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="Labels"/>
-            <MFXLegacyListView fx:id="labelView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0"
-                               prefWidth="150.0"/>
+            <MFXFlowlessListView fx:id="labelViewNew"/>
         </VBox>
         <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="265.0"
               spacing="10.0">
             <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="110.0" text="HBoxes"/>
-            <MFXLegacyListView fx:id="hBoxView" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="200.0"
-                               prefWidth="265.0"/>
+            <MFXFlowlessListView fx:id="hBoxViewNew"/>
         </VBox>
         <VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="250.0" prefWidth="135.0"
               spacing="10.0">
             <Label id="label" alignment="CENTER" prefHeight="25.0" prefWidth="125.0" text="Customized and CSS"/>
-            <MFXLegacyListView id="customView" fx:id="cssView" maxHeight="-Infinity" maxWidth="-Infinity"
-                               prefHeight="200.0" prefWidth="110.0" stylesheets="@css/listviews_demo.css"/>
+            <MFXFlowlessListView fx:id="cssViewNew"/>
         </VBox>
     </HBox>
-    <VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="90.0" prefWidth="128.0"
-          spacing="10.0" StackPane.alignment="BOTTOM_RIGHT">
+    <VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="90.0" prefWidth="128.0" spacing="10.0" StackPane.alignment="BOTTOM_RIGHT">
         <StackPane.margin>
-            <Insets bottom="5.0"/>
+            <Insets bottom="5.0" />
         </StackPane.margin>
-        <MFXButton fx:id="depthButton" prefWidth="70.0" text="3D"/>
-        <MFXButton fx:id="colorsButton" text="Change bars color"/>
+        <MFXButton fx:id="depthButton" prefWidth="70.0" text="3D" />
+        <MFXButton fx:id="colorsButton" text="Change bars color" />
     </VBox>
+   <MFXButton fx:id="switchButton" buttonType="RAISED" prefWidth="120.0" text="Switch to Legacy" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="75.0" />
+      </StackPane.margin>
+   </MFXButton>
 </StackPane>

+ 3 - 3
materialfx/build.gradle

@@ -8,9 +8,7 @@ repositories {
     jcenter()
 }
 
-
 compileJava {
-    // This is necessary since Gluon devs are assholes :)
     sourceCompatibility = '11'
     targetCompatibility = '11'
 }
@@ -20,6 +18,8 @@ dependencies {
     implementation 'com.vanniktech:gradle-maven-publish-plugin:0.13.0'
     implementation 'org.apache.logging.log4j:log4j-api:2.14.0'
     implementation 'org.apache.logging.log4j:log4j-core:2.14.0'
+
+    implementation 'org.reactfx:reactfx:2.0-M5'
 }
 
 javadoc {
@@ -33,7 +33,7 @@ javadoc {
     options.windowTitle = "$project.name $project.version API"
     options.docTitle = "$project.name $project.version API"
     options.links = ['https://docs.oracle.com/en/java/javase/11/docs/api',
-                     'https://openjfx.io/javadoc/14']
+                     'https://openjfx.io/javadoc/15']
 }
 
 task javadocJar(type: Jar, dependsOn: javadoc) {

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCheckTreeItem.java

@@ -22,8 +22,8 @@ import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeCell;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
 import io.github.palexdev.materialfx.controls.cell.MFXCheckTreeCell;
-import io.github.palexdev.materialfx.selection.ITreeCheckModel;
 import io.github.palexdev.materialfx.selection.TreeCheckModel;
+import io.github.palexdev.materialfx.selection.base.ITreeCheckModel;
 import io.github.palexdev.materialfx.skins.MFXCheckTreeItemSkin;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;

+ 308 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFlowlessListView.java

@@ -0,0 +1,308 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXFlowlessListCell;
+import io.github.palexdev.materialfx.controls.cell.MFXFlowlessListCell;
+import io.github.palexdev.materialfx.effects.DepthLevel;
+import io.github.palexdev.materialfx.selection.ListSelectionModel;
+import io.github.palexdev.materialfx.selection.base.IListSelectionModel;
+import io.github.palexdev.materialfx.skins.MFXFlowlessListViewSkin;
+import io.github.palexdev.materialfx.utils.ColorUtils;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.css.*;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.util.Callback;
+import javafx.util.Duration;
+
+import java.util.List;
+
+public class MFXFlowlessListView<T> extends Control {
+    private static final StyleablePropertyFactory<MFXFlowlessListView<?>> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-list-view";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-flowless-listview.css").toString();
+
+    private final ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<>();
+    private final ObjectProperty<Callback<T, AbstractMFXFlowlessListCell<T>>> cellFactory = new SimpleObjectProperty<>();
+    private final ObjectProperty<IListSelectionModel<T>> selectionModel = new SimpleObjectProperty<>();
+
+    public MFXFlowlessListView() {
+        this(FXCollections.observableArrayList());
+    }
+
+    public MFXFlowlessListView(ObservableList<T> items) {
+        setItems(items);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+
+        setCellFactory(MFXFlowlessListCell::new);
+        setupSelectionModel();
+        addListeners();
+    }
+
+    protected void setupSelectionModel() {
+        IListSelectionModel<T> selectionModel = new ListSelectionModel<>();
+        selectionModel.setAllowsMultipleSelection(true);
+        setSelectionModel(selectionModel);
+    }
+
+    /**
+     * Adds listeners for colors change to the scrollbars and calls setColors().
+     */
+    private void addListeners() {
+        this.trackColor.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                setColors();
+            }
+        });
+
+        this.thumbColor.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                setColors();
+            }
+        });
+
+        this.thumbHoverColor.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                setColors();
+            }
+        });
+    }
+
+    /**
+     * Sets the CSS looked-up colors
+     */
+    private void setColors() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("-mfx-track-color: ").append(ColorUtils.rgb((Color) trackColor.get()))
+                .append(";\n-mfx-thumb-color: ").append(ColorUtils.rgb((Color) thumbColor.get()))
+                .append(";\n-mfx-thumb-hover-color: ").append(ColorUtils.rgb((Color) thumbHoverColor.get()))
+                .append(";");
+        setStyle(sb.toString());
+    }
+
+    public ObservableList<T> getItems() {
+        return items.get();
+    }
+
+    public ObjectProperty<ObservableList<T>> itemsProperty() {
+        return items;
+    }
+
+    public void setItems(ObservableList<T> items) {
+        this.items.set(items);
+    }
+
+    public Callback<T, AbstractMFXFlowlessListCell<T>> getCellFactory() {
+        return cellFactory.get();
+    }
+
+    public ObjectProperty<Callback<T, AbstractMFXFlowlessListCell<T>>> cellFactoryProperty() {
+        return cellFactory;
+    }
+
+    public void setCellFactory(Callback<T, AbstractMFXFlowlessListCell<T>> cellFactory) {
+        this.cellFactory.set(cellFactory);
+    }
+
+    public IListSelectionModel<T> getSelectionModel() {
+        return selectionModel.get();
+    }
+
+    public ObjectProperty<IListSelectionModel<T>> selectionModelProperty() {
+        return selectionModel;
+    }
+
+    public void setSelectionModel(IListSelectionModel<T> selectionModel) {
+        this.selectionModel.set(selectionModel);
+    }
+
+    //================================================================================
+    // ScrollBars Properties
+    //================================================================================
+
+    /**
+     * Specifies the color of the scrollbars' track.
+     */
+    private final ObjectProperty<Paint> trackColor = new SimpleObjectProperty<>(Color.rgb(132, 132, 132));
+
+    /**
+     * Specifies the color of the scrollbars' thumb.
+     */
+    private final ObjectProperty<Paint> thumbColor = new SimpleObjectProperty<>(Color.rgb(137, 137, 137));
+
+    /**
+     * Specifies the color of the scrollbars' thumb when mouse hover.
+     */
+    private final ObjectProperty<Paint> thumbHoverColor = new SimpleObjectProperty<>(Color.rgb(89, 88, 91));
+
+    /**
+     * Specifies the time after which the scrollbars are hidden.
+     */
+    private final ObjectProperty<Duration> hideAfter = new SimpleObjectProperty<>(Duration.seconds(1));
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+
+    /**
+     * Specifies if the scrollbars should be hidden when the mouse is not on the list.
+     */
+    private final StyleableBooleanProperty hideScrollBars = new SimpleStyleableBooleanProperty(
+            StyleableProperties.HIDE_SCROLLBARS,
+            this,
+            "hideScrollBars",
+            false
+    );
+
+    /**
+     * Specifies the shadow strength around the control.
+     */
+    private final StyleableObjectProperty<DepthLevel> depthLevel = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.DEPTH_LEVEL,
+            this,
+            "depthLevel",
+            DepthLevel.LEVEL2
+    );
+
+    public Paint getTrackColor() {
+        return trackColor.get();
+    }
+
+    public ObjectProperty<Paint> trackColorProperty() {
+        return trackColor;
+    }
+
+    public void setTrackColor(Paint trackColor) {
+        this.trackColor.set(trackColor);
+    }
+
+    public Paint getThumbColor() {
+        return thumbColor.get();
+    }
+
+    public ObjectProperty<Paint> thumbColorProperty() {
+        return thumbColor;
+    }
+
+    public void setThumbColor(Paint thumbColor) {
+        this.thumbColor.set(thumbColor);
+    }
+
+    public Paint getThumbHoverColor() {
+        return thumbHoverColor.get();
+    }
+
+    public ObjectProperty<Paint> thumbHoverColorProperty() {
+        return thumbHoverColor;
+    }
+
+    public void setThumbHoverColor(Paint thumbHoverColor) {
+        this.thumbHoverColor.set(thumbHoverColor);
+    }
+
+    public Duration getHideAfter() {
+        return hideAfter.get();
+    }
+
+    public ObjectProperty<Duration> hideAfterProperty() {
+        return hideAfter;
+    }
+
+    public void setHideAfter(Duration hideAfter) {
+        this.hideAfter.set(hideAfter);
+    }
+
+    public boolean isHideScrollBars() {
+        return hideScrollBars.get();
+    }
+
+    public StyleableBooleanProperty hideScrollBarsProperty() {
+        return hideScrollBars;
+    }
+
+    public void setHideScrollBars(boolean hideScrollBars) {
+        this.hideScrollBars.set(hideScrollBars);
+    }
+
+    public DepthLevel getDepthLevel() {
+        return depthLevel.get();
+    }
+
+    public StyleableObjectProperty<DepthLevel> depthLevelProperty() {
+        return depthLevel;
+    }
+
+    public void setDepthLevel(DepthLevel depthLevel) {
+        this.depthLevel.set(depthLevel);
+    }
+
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXFlowlessListView<?>, Boolean> HIDE_SCROLLBARS =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-hide-scrollbars",
+                        MFXFlowlessListView::hideScrollBarsProperty,
+                        false
+                );
+
+        private static final CssMetaData<MFXFlowlessListView<?>, DepthLevel> DEPTH_LEVEL =
+                FACTORY.createEnumCssMetaData(
+                        DepthLevel.class,
+                        "-mfx-depth-level",
+                        MFXFlowlessListView::depthLevelProperty,
+                        DepthLevel.LEVEL2
+                );
+
+
+        static {
+            cssMetaDataList = List.of(HIDE_SCROLLBARS, DEPTH_LEVEL);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXFlowlessListViewSkin<>(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    protected List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXFlowlessListView.getControlCssMetaDataList();
+    }
+}

+ 16 - 16
materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyListView.java → materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXListView.java

@@ -16,12 +16,12 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.legacy;
+package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.cell.legacy.MFXLegacyListCell;
+import io.github.palexdev.materialfx.controls.cell.MFXListCell;
 import io.github.palexdev.materialfx.effects.DepthLevel;
-import io.github.palexdev.materialfx.skins.legacy.MFXLegacyListViewSkin;
+import io.github.palexdev.materialfx.skins.MFXListViewSkin;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
@@ -39,24 +39,24 @@ import java.util.List;
  * This is the implementation of a ListView restyled to comply with modern standards.
  * <p>
  * Extends {@code ListView}, redefines the style class to "mfx-list-view for usage in CSS,
- * for cells it uses {@link MFXLegacyListCell} by default.
+ * for cells it uses {@link MFXListCell} by default.
  */
-public class MFXLegacyListView<T> extends ListView<T> {
+public class MFXListView<T> extends ListView<T> {
     //================================================================================
     // Properties
     //================================================================================
-    private static final StyleablePropertyFactory<MFXLegacyListView<?>> FACTORY = new StyleablePropertyFactory<>(ListView.getClassCssMetaData());
+    private static final StyleablePropertyFactory<MFXListView<?>> FACTORY = new StyleablePropertyFactory<>(ListView.getClassCssMetaData());
     private final String STYLE_CLASS = "mfx-legacy-list-view";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/legacy/mfx-listview.css").toString();
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-listview.css").toString();
 
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXLegacyListView() {
+    public MFXListView() {
         initialize();
     }
 
-    public MFXLegacyListView(ObservableList<T> observableList) {
+    public MFXListView(ObservableList<T> observableList) {
         super(observableList);
         initialize();
     }
@@ -66,7 +66,7 @@ public class MFXLegacyListView<T> extends ListView<T> {
     //================================================================================
     private void initialize() {
         getStyleClass().add(STYLE_CLASS);
-        setCellFactory(cell -> new MFXLegacyListCell<>());
+        setCellFactory(cell -> new MFXListCell<>());
         addListeners();
     }
 
@@ -231,18 +231,18 @@ public class MFXLegacyListView<T> extends ListView<T> {
     private static class StyleableProperties {
         private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
 
-        private static final CssMetaData<MFXLegacyListView<?>, Boolean> HIDE_SCROLLBARS =
+        private static final CssMetaData<MFXListView<?>, Boolean> HIDE_SCROLLBARS =
                 FACTORY.createBooleanCssMetaData(
                         "-mfx-hide-scrollbars",
-                        MFXLegacyListView::hideScrollBarsProperty,
+                        MFXListView::hideScrollBarsProperty,
                         false
                 );
 
-        private static final CssMetaData<MFXLegacyListView<?>, DepthLevel> DEPTH_LEVEL =
+        private static final CssMetaData<MFXListView<?>, DepthLevel> DEPTH_LEVEL =
                 FACTORY.createEnumCssMetaData(
                         DepthLevel.class,
                         "-mfx-depth-level",
-                        MFXLegacyListView::depthLevelProperty,
+                        MFXListView::depthLevelProperty,
                         DepthLevel.LEVEL2
                 );
 
@@ -261,7 +261,7 @@ public class MFXLegacyListView<T> extends ListView<T> {
     //================================================================================
     @Override
     protected Skin<?> createDefaultSkin() {
-        return new MFXLegacyListViewSkin<>(this);
+        return new MFXListViewSkin<>(this);
     }
 
     @Override
@@ -271,6 +271,6 @@ public class MFXLegacyListView<T> extends ListView<T> {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return MFXLegacyListView.getControlCssMetaDataList();
+        return MFXListView.getControlCssMetaDataList();
     }
 }

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java

@@ -20,8 +20,8 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.cell.MFXTableColumnCell;
-import io.github.palexdev.materialfx.selection.ITableSelectionModel;
 import io.github.palexdev.materialfx.selection.TableSelectionModel;
+import io.github.palexdev.materialfx.selection.base.ITableSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXTableViewSkin;
 import javafx.beans.property.IntegerProperty;
 import javafx.beans.property.ObjectProperty;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTreeItem.java

@@ -22,8 +22,8 @@ import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeCell;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
 import io.github.palexdev.materialfx.controls.cell.MFXSimpleTreeCell;
-import io.github.palexdev.materialfx.selection.ITreeSelectionModel;
 import io.github.palexdev.materialfx.selection.TreeSelectionModel;
+import io.github.palexdev.materialfx.selection.base.ITreeSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXTreeItemSkin;
 import javafx.beans.property.*;
 import javafx.collections.ListChangeListener;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTreeView.java

@@ -20,8 +20,8 @@ package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
-import io.github.palexdev.materialfx.selection.ITreeSelectionModel;
 import io.github.palexdev.materialfx.selection.TreeSelectionModel;
+import io.github.palexdev.materialfx.selection.base.ITreeSelectionModel;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleBooleanProperty;

+ 110 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXFlowlessListCell.java

@@ -0,0 +1,110 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.base;
+
+import io.github.palexdev.materialfx.controls.flowless.Cell;
+import javafx.beans.property.*;
+import javafx.css.PseudoClass;
+import javafx.geometry.Pos;
+import javafx.scene.layout.HBox;
+
+public abstract class AbstractMFXFlowlessListCell<T> extends HBox implements Cell<T, HBox> {
+    private final ReadOnlyObjectProperty<T> data;
+    private final DoubleProperty fixedCellSize = new SimpleDoubleProperty();
+
+    private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
+    private final BooleanProperty selected = new SimpleBooleanProperty(false);
+
+    public AbstractMFXFlowlessListCell(T data) {
+        this(data, 32);
+    }
+
+    public AbstractMFXFlowlessListCell(T data, double fixedHeight) {
+        this.data = new ReadOnlyObjectWrapper<>(data);
+        this.fixedCellSize.set(fixedHeight);
+
+        setMinHeight(USE_PREF_SIZE);
+        setMaxHeight(USE_PREF_SIZE);
+        prefHeightProperty().bind(fixedCellSize);
+
+        initialize();
+        render(data);
+    }
+
+    private void initialize() {
+        setAlignment(Pos.CENTER_LEFT);
+        setSpacing(5);
+
+        addListeners();
+    }
+
+    private void addListeners() {
+        selected.addListener(invalidate -> pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, selected.get()));
+    }
+
+    public T getData() {
+        return data.get();
+    }
+
+    public ReadOnlyObjectProperty<T> dataProperty() {
+        return data;
+    }
+
+    public DoubleProperty fixedCellSizeProperty() {
+        return fixedCellSize;
+    }
+
+    public void setFixedCellSize(double fixedCellSize) {
+        this.fixedCellSize.set(fixedCellSize);
+    }
+
+    public boolean isSelected() {
+        return selected.get();
+    }
+
+    public BooleanProperty selectedProperty() {
+        return selected;
+    }
+
+    public void setSelected(boolean selected) {
+        this.selected.set(selected);
+    }
+
+    protected abstract void render(T data);
+
+    @Override
+    public HBox getNode() {
+        return this;
+    }
+
+    @Override
+    public boolean isReusable() {
+        return true;
+    }
+
+    @Override
+    public void reset() {
+        getChildren().clear();
+    }
+
+    @Override
+    public void updateItem(T data) {
+        render(data);
+    }
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXTreeItem.java

@@ -19,7 +19,7 @@
 package io.github.palexdev.materialfx.controls.base;
 
 import io.github.palexdev.materialfx.controls.MFXTreeView;
-import io.github.palexdev.materialfx.selection.ITreeSelectionModel;
+import io.github.palexdev.materialfx.selection.base.ITreeSelectionModel;
 import io.github.palexdev.materialfx.utils.TreeItemStream;
 import javafx.beans.property.*;
 import javafx.collections.FXCollections;

+ 92 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXFlowlessListCell.java

@@ -0,0 +1,92 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.cell;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXFlowlessListCell;
+import io.github.palexdev.materialfx.effects.RippleGenerator;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+
+public class MFXFlowlessListCell<T> extends AbstractMFXFlowlessListCell<T> {
+    private final String STYLE_CLASS = "mfx-list-cell";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-flowless-listcell.css").toString();
+    protected final RippleGenerator rippleGenerator = new RippleGenerator(this);
+
+    public MFXFlowlessListCell(T data) {
+        super(data);
+        initialize();
+    }
+
+    public MFXFlowlessListCell(T data, double fixedHeight) {
+        super(data, fixedHeight);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        setupRippleGenerator();
+    }
+
+    protected void setupRippleGenerator() {
+        rippleGenerator.setManaged(false);
+        rippleGenerator.rippleRadiusProperty().bind(widthProperty().divide(2.0));
+
+        addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            rippleGenerator.setGeneratorCenterX(event.getX());
+            rippleGenerator.setGeneratorCenterY(event.getY());
+            rippleGenerator.createRipple();
+        });
+        getChildren().add(0, rippleGenerator);
+    }
+
+    @Override
+    protected void render(T data) {
+        if (data instanceof Node) {
+            getChildren().setAll((Node) data);
+        } else {
+            Label label = new Label(data.toString());
+            label.getStyleClass().add("data-label");
+            getChildren().setAll(label);
+        }
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public String toString() {
+        String className = getClass().getName();
+        String simpleName = className.substring(className.lastIndexOf('.') + 1);
+        StringBuilder sb = new StringBuilder();
+        sb.append("[").append(simpleName);
+        sb.append('@');
+        sb.append(Integer.toHexString(hashCode()));
+        sb.append("]");
+        sb.append("[Data:").append(getData()).append("]");
+        if (getId() != null) {
+            sb.append("[id:").append(getId()).append("]");
+        }
+
+        return sb.toString();
+    }
+}

+ 19 - 19
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/legacy/MFXLegacyListCell.java → materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXListCell.java

@@ -16,7 +16,7 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.controls.cell.legacy;
+package io.github.palexdev.materialfx.controls.cell;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.effects.RippleGenerator;
@@ -42,19 +42,19 @@ import java.util.stream.Collectors;
  * Extends {@code ListCell}, redefines the style class to "mfx-list-cell" for usage in CSS,
  * each cell has a {@code RippleGenerator} to generate ripple effects on click.
  */
-public class MFXLegacyListCell<T> extends ListCell<T> {
+public class MFXListCell<T> extends ListCell<T> {
     //================================================================================
     // Properties
     //================================================================================
-    private static final StyleablePropertyFactory<MFXLegacyListCell<?>> FACTORY = new StyleablePropertyFactory<>(ListCell.getClassCssMetaData());
+    private static final StyleablePropertyFactory<MFXListCell<?>> FACTORY = new StyleablePropertyFactory<>(ListCell.getClassCssMetaData());
     private final String STYLE_CLASS = "mfx-list-cell";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/legacy/mfx-listcell.css").toString();
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-listcell.css").toString();
     private final RippleGenerator rippleGenerator;
 
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXLegacyListCell() {
+    public MFXListCell() {
         rippleGenerator = new RippleGenerator(this);
         rippleGenerator.setRippleColor(Color.rgb(50, 150, 255));
         rippleGenerator.setInDuration(Duration.millis(400));
@@ -90,9 +90,9 @@ public class MFXLegacyListCell<T> extends ListCell<T> {
 
         selectedProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue) {
-                NodeUtils.updateBackground(MFXLegacyListCell.this, getSelectedColor(), new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
+                NodeUtils.updateBackground(MFXListCell.this, getSelectedColor(), new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
             } else {
-                NodeUtils.updateBackground(MFXLegacyListCell.this, Color.WHITE, new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
+                NodeUtils.updateBackground(MFXListCell.this, Color.WHITE, new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
             }
         });
 
@@ -111,16 +111,16 @@ public class MFXLegacyListCell<T> extends ListCell<T> {
                 if (getIndex() == 0) {
                     setBackground(new Background(new BackgroundFill(getHoverColor(), CornerRadii.EMPTY, Insets.EMPTY)));
                 } else {
-                    NodeUtils.updateBackground(MFXLegacyListCell.this, getHoverColor(), new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
+                    NodeUtils.updateBackground(MFXListCell.this, getHoverColor(), new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
                 }
             } else {
-                NodeUtils.updateBackground(MFXLegacyListCell.this, Color.WHITE, new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
+                NodeUtils.updateBackground(MFXListCell.this, Color.WHITE, new CornerRadii(getCornerRadius()), new Insets(getBackgroundInsets()));
             }
         });
 
         selectedColor.addListener((observableValue, oldValue, newValue) -> {
             if (!newValue.equals(oldValue) && isSelected()) {
-                NodeUtils.updateBackground(MFXLegacyListCell.this, newValue);
+                NodeUtils.updateBackground(MFXListCell.this, newValue);
             }
         });
 
@@ -235,31 +235,31 @@ public class MFXLegacyListCell<T> extends ListCell<T> {
     private static class StyleableProperties {
         private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
 
-        private static final CssMetaData<MFXLegacyListCell<?>, Paint> SELECTED_COLOR =
+        private static final CssMetaData<MFXListCell<?>, Paint> SELECTED_COLOR =
                 FACTORY.createPaintCssMetaData(
                         "-mfx-selected-color",
-                        MFXLegacyListCell::selectedColorProperty,
+                        MFXListCell::selectedColorProperty,
                         Color.rgb(180, 180, 255)
                 );
 
-        private static final CssMetaData<MFXLegacyListCell<?>, Paint> HOVER_COLOR =
+        private static final CssMetaData<MFXListCell<?>, Paint> HOVER_COLOR =
                 FACTORY.createPaintCssMetaData(
                         "-mfx-hover-color",
-                        MFXLegacyListCell::hoverColorProperty,
+                        MFXListCell::hoverColorProperty,
                         Color.rgb(50, 150, 255, 0.2)
                 );
 
-        private static final CssMetaData<MFXLegacyListCell<?>, Number> CORNER_RADIUS =
+        private static final CssMetaData<MFXListCell<?>, Number> CORNER_RADIUS =
                 FACTORY.createSizeCssMetaData(
                         "-mfx-corner-radius",
-                        MFXLegacyListCell::cornerRadiusProperty,
+                        MFXListCell::cornerRadiusProperty,
                         0
                 );
 
-        private static final CssMetaData<MFXLegacyListCell<?>, Number> BACKGROUND_INSETS =
+        private static final CssMetaData<MFXListCell<?>, Number> BACKGROUND_INSETS =
                 FACTORY.createSizeCssMetaData(
                         "-mfx-background-insets",
-                        MFXLegacyListCell::backgroundInsetsProperty,
+                        MFXListCell::backgroundInsetsProperty,
                         0
                 );
 
@@ -283,7 +283,7 @@ public class MFXLegacyListCell<T> extends ListCell<T> {
 
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return MFXLegacyListCell.getControlCssMetaDataList();
+        return MFXListCell.getControlCssMetaDataList();
     }
 
     /**

+ 133 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/Cell.java

@@ -0,0 +1,133 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.scene.Node;
+
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+/**
+ * Provides efficient memory usage by wrapping a {@link Node} within this object and reusing it when
+ * {@link #isReusable()} is true.
+ */
+@FunctionalInterface
+public interface Cell<T, N extends Node> {
+    static <T, N extends Node> Cell<T, N> wrapNode(N node) {
+        return new Cell<>() {
+
+            @Override
+            public N getNode() {
+                return node;
+            }
+
+            @Override
+            public String toString() {
+                return node.toString();
+            }
+        };
+    }
+
+    N getNode();
+
+    /**
+     * Indicates whether this cell can be reused to display different items.
+     *
+     * <p>Default implementation returns {@code false}.
+     */
+    default boolean isReusable() {
+        return false;
+    }
+
+    /**
+     * If this cell is reusable (as indicated by {@link #isReusable()}),
+     * this method is called to display a different item. {@link #reset()}
+     * will have been called before a call to this method.
+     *
+     * <p>The default implementation throws
+     * {@link UnsupportedOperationException}.
+     *
+     * @param item the new item to display
+     */
+    default void updateItem(T item) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Called to update index of a visible cell.
+     *
+     * <p>Default implementation does nothing.
+     */
+    default void updateIndex(int index) {
+        // do nothing by default
+    }
+
+    /**
+     * Called when this cell is no longer used to display its item.
+     * If this cell is reusable, it may later be asked to display a different
+     * item by a call to {@link #updateItem(Object)}.
+     *
+     * <p>Default implementation does nothing.
+     */
+    default void reset() {
+        // do nothing by default
+    }
+
+    /**
+     * Called when this cell is no longer going to be used at all.
+     * {@link #reset()} will have been called before this method is invoked.
+     *
+     * <p>Default implementation does nothing.
+     */
+    default void dispose() {
+        // do nothing by default
+    }
+
+    default Cell<T, N> beforeDispose(Runnable action) {
+        return CellWrapper.beforeDispose(this, action);
+    }
+
+    default Cell<T, N> afterDispose(Runnable action) {
+        return CellWrapper.afterDispose(this, action);
+    }
+
+    default Cell<T, N> beforeReset(Runnable action) {
+        return CellWrapper.beforeReset(this, action);
+    }
+
+    default Cell<T, N> afterReset(Runnable action) {
+        return CellWrapper.afterReset(this, action);
+    }
+
+    default Cell<T, N> beforeUpdateItem(Consumer<? super T> action) {
+        return CellWrapper.beforeUpdateItem(this, action);
+    }
+
+    default Cell<T, N> afterUpdateItem(Consumer<? super T> action) {
+        return CellWrapper.afterUpdateItem(this, action);
+    }
+
+    default Cell<T, N> beforeUpdateIndex(IntConsumer action) {
+        return CellWrapper.beforeUpdateIndex(this, action);
+    }
+
+    default Cell<T, N> afterUpdateIndex(IntConsumer action) {
+        return CellWrapper.afterUpdateIndex(this, action);
+    }
+}

+ 163 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellListManager.java

@@ -0,0 +1,163 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.input.ScrollEvent;
+import org.reactfx.EventStreams;
+import org.reactfx.Subscription;
+import org.reactfx.collection.LiveList;
+import org.reactfx.collection.MemoizationList;
+import org.reactfx.collection.QuasiListModification;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Tracks all of the cells that the viewport can display ({@link #cells}) and which cells the viewport is currently
+ * displaying ({@link #presentCells}).
+ */
+final class CellListManager<T, C extends Cell<T, ? extends Node>> {
+
+    private final Node owner;
+    private final CellPool<T, C> cellPool;
+    private final MemoizationList<C> cells;
+    private final LiveList<C> presentCells;
+    private final LiveList<Node> cellNodes;
+
+    private final Subscription presentCellsSubscription;
+
+    public CellListManager(
+            Node owner,
+            ObservableList<T> items,
+            Function<? super T, ? extends C> cellFactory) {
+        this.owner = owner;
+        this.cellPool = new CellPool<>(cellFactory);
+        this.cells = LiveList.map(items, this::cellForItem).memoize();
+        this.presentCells = cells.memoizedItems();
+        this.cellNodes = presentCells.map(Cell::getNode);
+        this.presentCellsSubscription = presentCells.observeQuasiModifications(this::presentCellsChanged);
+    }
+
+    public void dispose() {
+        // return present cells to pool *before* unsubscribing,
+        // because stopping to observe memoized items may clear memoized items
+        presentCells.forEach(cellPool::acceptCell);
+        presentCellsSubscription.unsubscribe();
+        cellPool.dispose();
+    }
+
+    /**
+     * Gets the list of nodes that the viewport is displaying
+     */
+    public ObservableList<Node> getNodes() {
+        return cellNodes;
+    }
+
+    public MemoizationList<C> getLazyCellList() {
+        return cells;
+    }
+
+    public boolean isCellPresent(int itemIndex) {
+        return cells.isMemoized(itemIndex);
+    }
+
+    public C getPresentCell(int itemIndex) {
+        // both getIfMemoized() and get() may throw
+        return cells.getIfMemoized(itemIndex).get();
+    }
+
+    public Optional<C> getCellIfPresent(int itemIndex) {
+        return cells.getIfMemoized(itemIndex); // getIfMemoized() may throw
+    }
+
+    public C getCell(int itemIndex) {
+        return cells.get(itemIndex);
+    }
+
+    /**
+     * Updates the list of cells to display
+     *
+     * @param fromItem the index of the first item to display
+     * @param toItem   the index of the last item to display
+     */
+    public void cropTo(int fromItem, int toItem) {
+        fromItem = Math.max(fromItem, 0);
+        toItem = Math.min(toItem, cells.size());
+        cells.forget(0, fromItem);
+        cells.forget(toItem, cells.size());
+    }
+
+    private C cellForItem(T item) {
+        C cell = cellPool.getCell(item);
+
+        // apply CSS when the cell is first added to the scene
+        Node node = cell.getNode();
+        EventStreams.nonNullValuesOf(node.sceneProperty())
+                .subscribeForOne(scene -> node.applyCss());
+
+        // Make cell initially invisible.
+        // It will be made visible when it is positioned.
+        node.setVisible(false);
+
+        if (cell.isReusable()) {
+            // if cell is reused i think adding event handler
+            // would cause resource leakage.
+            node.setOnScroll(this::pushScrollEvent);
+            node.setOnScrollStarted(this::pushScrollEvent);
+            node.setOnScrollFinished(this::pushScrollEvent);
+        } else {
+            node.addEventHandler(ScrollEvent.ANY, this::pushScrollEvent);
+        }
+
+        return cell;
+    }
+
+    /**
+     * Push scroll events received by cell nodes directly to
+     * the 'owner' Node. (Generally likely to be a VirtualFlow
+     * but not required.)
+     * <p>
+     * Normal bubbling of scroll events gets interrupted during
+     * a scroll gesture when the Cell's Node receiving the event
+     * has moved out of the viewport and is thus removed from
+     * the Navigator's children list. This breaks expected trackpad
+     * scrolling behaviour, at least on macOS.
+     * <p>
+     * So here we take over event-bubbling duties for ScrollEvent
+     * and push them ourselves directly to the given owner.
+     */
+    private void pushScrollEvent(ScrollEvent se) {
+        owner.fireEvent(se);
+        se.consume();
+    }
+
+    private void presentCellsChanged(QuasiListModification<? extends C> mod) {
+        // add removed cells back to the pool
+        for (C cell : mod.getRemoved()) {
+            cellPool.acceptCell(cell);
+        }
+
+        // update indices of added cells and cells after the added cells
+        for (int i = mod.getFrom(); i < presentCells.size(); ++i) {
+            presentCells.get(i).updateIndex(cells.indexOfMemoizedItem(i));
+        }
+    }
+}

+ 74 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellPool.java

@@ -0,0 +1,74 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.function.Function;
+
+/**
+ * Helper class that stores a pool of reusable cells that can be updated via {@link Cell#updateItem(Object)} or
+ * creates new ones via its {@link #cellFactory} if the pool is empty.
+ */
+final class CellPool<T, C extends Cell<T, ?>> {
+    private final Function<? super T, ? extends C> cellFactory;
+    private final Queue<C> pool = new LinkedList<>();
+
+    public CellPool(Function<? super T, ? extends C> cellFactory) {
+        this.cellFactory = cellFactory;
+    }
+
+    /**
+     * Returns a reusable cell that has been updated with the current item if the pool has one, or returns a
+     * newly-created one via its {@link #cellFactory}.
+     */
+    public C getCell(T item) {
+        C cell = pool.poll();
+        if (cell != null) {
+            cell.updateItem(item);
+        } else {
+            cell = cellFactory.apply(item);
+        }
+        return cell;
+    }
+
+    /**
+     * Adds the cell to the pool of reusable cells if {@link Cell#isReusable()} is true, or
+     * {@link Cell#dispose() disposes} the cell if it's not.
+     */
+    public void acceptCell(C cell) {
+        cell.reset();
+        if (cell.isReusable()) {
+            pool.add(cell);
+        } else {
+            cell.dispose();
+        }
+    }
+
+    /**
+     * Disposes the cell pool and prevents any memory leaks.
+     */
+    public void dispose() {
+        for (C cell : pool) {
+            cell.dispose();
+        }
+
+        pool.clear();
+    }
+}

+ 245 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellPositioner.java

@@ -0,0 +1,245 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import org.reactfx.collection.MemoizationList;
+
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.OptionalInt;
+
+/**
+ * Helper class for properly {@link javafx.scene.Node#resize(double, double) resizing} and
+ * {@link javafx.scene.Node#relocate(double, double) relocating} a {@link Cell}'s {@link javafx.scene.Node} as well
+ * as handling calls related to whether a cell's node is visible (displayed in the viewport) or not.
+ */
+final class CellPositioner<T, C extends Cell<T, ?>> {
+    private final CellListManager<T, C> cellManager;
+    private final OrientationHelper orientation;
+    private final SizeTracker sizeTracker;
+
+    public CellPositioner(
+            CellListManager<T, C> cellManager,
+            OrientationHelper orientation,
+            SizeTracker sizeTracker) {
+        this.cellManager = cellManager;
+        this.orientation = orientation;
+        this.sizeTracker = sizeTracker;
+    }
+
+    public void cropTo(int from, int to) {
+        cellManager.cropTo(from, to);
+    }
+
+    public C getVisibleCell(int itemIndex) {
+        C cell = cellManager.getPresentCell(itemIndex);
+        if (cell.getNode().isVisible()) {
+            return cell;
+        } else {
+            throw new NoSuchElementException(
+                    "Cell " + itemIndex + " is not visible");
+        }
+    }
+
+    public Optional<C> getCellIfVisible(int itemIndex) {
+        return cellManager.getCellIfPresent(itemIndex)
+                .filter(c -> c.getNode().isVisible());
+    }
+
+    public OptionalInt lastVisibleBefore(int position) {
+        MemoizationList<C> cells = cellManager.getLazyCellList();
+        int presentBefore = cells.getMemoizedCountBefore(position);
+        for (int i = presentBefore - 1; i >= 0; --i) {
+            C cell = cells.memoizedItems().get(i);
+            if (cell.getNode().isVisible()) {
+                return OptionalInt.of(cells.indexOfMemoizedItem(i));
+            }
+        }
+        return OptionalInt.empty();
+    }
+
+    public OptionalInt firstVisibleAfter(int position) {
+        MemoizationList<C> cells = cellManager.getLazyCellList();
+        int presentBefore = cells.getMemoizedCountBefore(position);
+        int present = cells.getMemoizedCount();
+        for (int i = presentBefore; i < present; ++i) {
+            C cell = cells.memoizedItems().get(i);
+            if (cell.getNode().isVisible()) {
+                return OptionalInt.of(cells.indexOfMemoizedItem(i));
+            }
+        }
+        return OptionalInt.empty();
+    }
+
+    public OptionalInt getLastVisibleIndex() {
+        return lastVisibleBefore(cellManager.getLazyCellList().size());
+    }
+
+    public OptionalInt getFirstVisibleIndex() {
+        return firstVisibleAfter(0);
+    }
+
+    /**
+     * Gets the shortest delta amount by which to scroll the viewport's length in order to fully display a
+     * partially-displayed cell's node.
+     */
+    public double shortestDeltaToViewport(C cell) {
+        return shortestDeltaToViewport(cell, 0.0, orientation.length(cell));
+    }
+
+    public double shortestDeltaToViewport(C cell, double fromY, double toY) {
+        double cellMinY = orientation.minY(cell);
+        double gapBefore = cellMinY + fromY;
+        double gapAfter = sizeTracker.getViewportLength() - (cellMinY + toY);
+
+        return (gapBefore < 0 && gapAfter > 0) ? Math.min(-gapBefore, gapAfter) :
+                (gapBefore > 0 && gapAfter < 0) ? Math.max(-gapBefore, gapAfter) :
+                        0.0;
+    }
+
+    /**
+     * Moves the given cell's node's "layoutY" value by {@code delta}. See {@link OrientationHelper}'s javadoc for more
+     * explanation on what quoted terms mean.
+     */
+    public void shiftCellBy(C cell, double delta) {
+        double y = orientation.minY(cell) + delta;
+        relocate(cell, 0, y);
+    }
+
+    /**
+     * Properly resizes the cell's node, and sets its "layoutY" value, so that is the first visible
+     * node in the viewport, and further offsets this value by {@code startOffStart}, so that
+     * the node's <em>top</em> edge appears (if negative) "above," (if 0) "at," or (if negative) "below" the viewport's
+     * "top" edge. See {@link OrientationHelper}'s javadoc for more explanation on what quoted terms mean.
+     *
+     * <pre><code>
+     *      --------- top of cell's node if startOffStart is negative
+     *
+     *     __________ "top edge" of viewport / top of cell's node if startOffStart = 0
+     *     |
+     *     |
+     *     |--------- top of cell's node if startOffStart is positive
+     *     |
+     * </code></pre>
+     *
+     * @param itemIndex     the index of the item in the list of all (not currently visible) cells
+     * @param startOffStart the amount by which to offset the "layoutY" value of the cell's node
+     */
+    public C placeStartAt(int itemIndex, double startOffStart) {
+        C cell = getSizedCell(itemIndex);
+        relocate(cell, 0, startOffStart);
+        cell.getNode().setVisible(true);
+        return cell;
+    }
+
+    /**
+     * Properly resizes the cell's node, and sets its "layoutY" value, so that is the last visible
+     * node in the viewport, and further offsets this value by {@code endOffStart}, so that
+     * the node's <em>top</em> edge appears (if negative) "above," (if 0) "at," or (if negative) "below" the
+     * viewport's "bottom" edge. See {@link OrientationHelper}'s javadoc for more explanation on what quoted terms mean.
+     *
+     * <pre><code>
+     *     |--------- top of cell's node if endOffStart is negative
+     *     |
+     *     |
+     *     |_________ "bottom edge" of viewport / top of cell's node if endOffStart = 0
+     *
+     *
+     *      --------- top of cell's node if endOffStart is positive
+     * </code></pre>
+     *
+     * @param itemIndex   the index of the item in the list of all (not currently visible) cells
+     * @param endOffStart the amount by which to offset the "layoutY" value of the cell's node
+     */
+    public C placeEndFromStart(int itemIndex, double endOffStart) {
+        C cell = getSizedCell(itemIndex);
+        relocate(cell, 0, endOffStart - orientation.length(cell));
+        cell.getNode().setVisible(true);
+        return cell;
+    }
+
+    /**
+     * Properly resizes the cell's node, and sets its "layoutY" value, so that is the last visible
+     * node in the viewport, and further offsets this value by {@code endOffStart}, so that
+     * the node's <em>bottom</em> edge appears (if negative) "above," (if 0) "at," or (if negative) "below" the
+     * viewport's "bottom" edge. See {@link OrientationHelper}'s javadoc for more explanation on what quoted terms mean.
+     *
+     * <pre><code>
+     *     |--------- bottom of cell's node if endOffEnd is negative
+     *     |
+     *     |_________ "bottom edge" of viewport / bottom of cell's node if endOffEnd = 0
+     *
+     *
+     *      --------- bottom of cell's node if endOffEnd is positive
+     * </code></pre>
+     *
+     * @param itemIndex the index of the item in the list of all (not currently visible) cells
+     * @param endOffEnd the amount by which to offset the "layoutY" value of the cell's node
+     */
+    public C placeEndFromEnd(int itemIndex, double endOffEnd) {
+        C cell = getSizedCell(itemIndex);
+        double y = sizeTracker.getViewportLength() + endOffEnd - orientation.length(cell);
+        relocate(cell, 0, y);
+        cell.getNode().setVisible(true);
+        return cell;
+    }
+
+    /**
+     * Properly resizes the cell's node, and sets its "layoutY" value, so that is the last visible
+     * node in the viewport, and further offsets this value by {@code endOffStart}, so that
+     * the node's <em>bottom</em> edge appears (if negative) "above," (if 0) "at," or (if negative) "below" the
+     * viewport's "top" edge. See {@link OrientationHelper}'s javadoc for more explanation on what quoted terms mean.
+     *
+     * <pre><code>
+     *      --------- bottom of cell's node if startOffStart is negative
+     *
+     *     __________ "top edge" of viewport / bottom of cell's node if startOffStart = 0
+     *     |
+     *     |
+     *     |--------- bottom of cell's node if startOffStart is positive
+     *     |
+     * </code></pre>
+     *
+     * @param itemIndex   the index of the item in the list of all (not currently visible) cells
+     * @param startOffEnd the amount by which to offset the "layoutY" value of the cell's node
+     */
+    public C placeStartFromEnd(int itemIndex, double startOffEnd) {
+        C cell = getSizedCell(itemIndex);
+        double y = sizeTracker.getViewportLength() + startOffEnd;
+        relocate(cell, 0, y);
+        cell.getNode().setVisible(true);
+        return cell;
+    }
+
+    /**
+     * Returns properly sized, but not properly positioned cell for the given
+     * index.
+     */
+    C getSizedCell(int itemIndex) {
+        C cell = cellManager.getCell(itemIndex);
+        double breadth = sizeTracker.breadthFor(itemIndex);
+        double length = sizeTracker.lengthFor(itemIndex);
+        orientation.resize(cell, breadth, length);
+        return cell;
+    }
+
+    private void relocate(C cell, double breadth0, double length0) {
+        orientation.relocate(cell, breadth0, length0);
+    }
+}

+ 164 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/CellWrapper.java

@@ -0,0 +1,164 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.scene.Node;
+
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+/**
+ * Factory class for wrapping a {@link Cell} and running additional code before/after specific methods
+ */
+abstract class CellWrapper<T, N extends Node, C extends Cell<T, N>>
+        implements Cell<T, N> {
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> beforeDispose(C cell, Runnable action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void dispose() {
+                action.run();
+                super.dispose();
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> afterDispose(C cell, Runnable action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void dispose() {
+                super.dispose();
+                action.run();
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> beforeReset(C cell, Runnable action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void reset() {
+                action.run();
+                super.reset();
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> afterReset(C cell, Runnable action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void reset() {
+                super.reset();
+                action.run();
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> beforeUpdateItem(C cell, Consumer<? super T> action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void updateItem(T item) {
+                action.accept(item);
+                super.updateItem(item);
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> afterUpdateItem(C cell, Consumer<? super T> action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void updateItem(T item) {
+                super.updateItem(item);
+                action.accept(item);
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> beforeUpdateIndex(C cell, IntConsumer action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void updateIndex(int index) {
+                action.accept(index);
+                super.updateIndex(index);
+            }
+        };
+    }
+
+    public static <T, N extends Node, C extends Cell<T, N>>
+    CellWrapper<T, N, C> afterUpdateIndex(C cell, IntConsumer action) {
+        return new CellWrapper<>(cell) {
+            @Override
+            public void updateIndex(int index) {
+                super.updateIndex(index);
+                action.accept(index);
+            }
+        };
+    }
+
+    private final C delegate;
+
+    public CellWrapper(C delegate) {
+        this.delegate = delegate;
+    }
+
+    public C getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public N getNode() {
+        return delegate.getNode();
+    }
+
+    @Override
+    public boolean isReusable() {
+        return delegate.isReusable();
+    }
+
+    @Override
+    public void updateItem(T item) {
+        delegate.updateItem(item);
+    }
+
+    @Override
+    public void updateIndex(int index) {
+        delegate.updateIndex(index);
+    }
+
+    @Override
+    public void reset() {
+        delegate.reset();
+    }
+
+    @Override
+    public void dispose() {
+        delegate.dispose();
+    }
+
+    @Override
+    public String toString() {
+        return delegate.toString();
+    }
+}

+ 87 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/MFXVirtualizedScrollPane.java

@@ -0,0 +1,87 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class MFXVirtualizedScrollPane<V extends Node & Virtualized> extends VirtualizedScrollPane<V> {
+
+    public MFXVirtualizedScrollPane(V content, ScrollPane.ScrollBarPolicy hPolicy, ScrollPane.ScrollBarPolicy vPolicy) {
+        super(content, hPolicy, vPolicy);
+        initialize();
+    }
+
+    public MFXVirtualizedScrollPane(V content) {
+        super(content);
+        initialize();
+    }
+
+    private void initialize() {
+        sceneProperty().addListener(new ChangeListener<>() {
+            @Override
+            public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
+                Set<ScrollBar> scrollBars = lookupAll(".scroll-bar").stream()
+                        .filter(node -> node instanceof ScrollBar)
+                        .map(node -> (ScrollBar) node)
+                        .collect(Collectors.toSet());
+                for (ScrollBar scrollBar : scrollBars) {
+                    manageScrollBarBackground(scrollBar);
+                    NodeUtils.updateBackground(scrollBar, Color.WHITE);
+                }
+                sceneProperty().removeListener(this);
+            }
+        });
+
+        try {
+            Field vbarField = VirtualizedScrollPane.class.getDeclaredField("vBar");
+            vbarField.setAccessible(true);
+            ScrollBar scrollBar = (ScrollBar) vbarField.get(this);
+            scrollBar.getStyleClass().add("vvbar");
+        } catch (Exception ignored) {
+        }
+    }
+
+    private void manageScrollBarBackground(ScrollBar scrollBar) {
+        scrollBar.backgroundProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null && !containsFill(newValue.getFills(), Color.WHITE)) {
+                NodeUtils.updateBackground(scrollBar, Color.WHITE);
+            }
+        });
+    }
+
+    private boolean containsFill(List<BackgroundFill> backgroundFills, Color fill) {
+        List<Paint> paints = backgroundFills.stream().map(BackgroundFill::getFill).collect(Collectors.toList());
+        return paints.contains(fill);
+    }
+}

+ 423 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/Navigator.java

@@ -0,0 +1,423 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import io.github.palexdev.materialfx.controls.flowless.VirtualFlow.Gravity;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.ObjectProperty;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.layout.Region;
+import org.reactfx.Subscription;
+import org.reactfx.collection.LiveList;
+import org.reactfx.collection.MemoizationList;
+import org.reactfx.collection.QuasiListChange;
+import org.reactfx.collection.QuasiListModification;
+
+import java.util.Optional;
+import java.util.OptionalInt;
+
+/**
+ * Responsible for laying out cells' nodes within the viewport based on a single anchor node. In a layout call,
+ * this anchor node is positioned in the viewport before any other node and then nodes are positioned above and
+ * below that anchor node sequentially. This sequential layout continues until the viewport's "top" and "bottom" edges
+ * are reached or there are no other cells' nodes to render. In this latter case (when there is not enough content to
+ * fill up the entire viewport), the displayed cells are repositioned towards the "ground," based on the
+ * {@link VirtualFlow}'s {@link Gravity} value, and any remaining unused space counts as the "sky."
+ */
+final class Navigator<T, C extends Cell<T, ?>> extends Region implements TargetPositionVisitor {
+    private final CellListManager<T, C> cellListManager;
+    private final MemoizationList<C> cells;
+    private final CellPositioner<T, C> positioner;
+    private final OrientationHelper orientation;
+    private final ObjectProperty<Gravity> gravity;
+    private final SizeTracker sizeTracker;
+    private final Subscription itemsSubscription;
+
+    private TargetPosition currentPosition = TargetPosition.BEGINNING;
+    private TargetPosition targetPosition = TargetPosition.BEGINNING;
+    private int firstVisibleIndex = -1;
+    private int lastVisibleIndex = -1;
+
+    public Navigator(
+            CellListManager<T, C> cellListManager,
+            CellPositioner<T, C> positioner,
+            OrientationHelper orientation,
+            ObjectProperty<Gravity> gravity,
+            SizeTracker sizeTracker) {
+        this.cellListManager = cellListManager;
+        this.cells = cellListManager.getLazyCellList();
+        this.positioner = positioner;
+        this.orientation = orientation;
+        this.gravity = gravity;
+        this.sizeTracker = sizeTracker;
+
+        this.itemsSubscription = LiveList.observeQuasiChanges(cellListManager.getLazyCellList(), this::itemsChanged);
+        Bindings.bindContent(getChildren(), cellListManager.getNodes());
+        // When gravity changes, we must redo our layout:
+        gravity.addListener((prop, oldVal, newVal) -> requestLayout());
+    }
+
+    public void dispose() {
+        itemsSubscription.unsubscribe();
+        Bindings.unbindContent(getChildren(), cellListManager.getNodes());
+    }
+
+    @Override
+    protected void layoutChildren() {
+        // invalidate breadth for each cell that has dirty layout
+        int n = cells.getMemoizedCount();
+        for (int i = 0; i < n; ++i) {
+            int j = cells.indexOfMemoizedItem(i);
+            Node node = cells.get(j).getNode();
+            if (node instanceof Parent && ((Parent) node).isNeedsLayout()) {
+                sizeTracker.forgetSizeOf(j);
+            }
+        }
+
+        if (!cells.isEmpty()) {
+            targetPosition.clamp(cells.size())
+                    .accept(this);
+        }
+        currentPosition = getCurrentPosition();
+        targetPosition = currentPosition;
+    }
+
+    /**
+     * Sets the {@link TargetPosition} used to layout the anchor node and re-lays out the viewport
+     */
+    public void setTargetPosition(TargetPosition targetPosition) {
+        this.targetPosition = targetPosition;
+        requestLayout();
+    }
+
+    /**
+     * Sets the {@link TargetPosition} used to layout the anchor node to the current position scrolled by {@code delta}
+     * and re-lays out the viewport
+     */
+    public void scrollCurrentPositionBy(double delta) {
+        targetPosition = currentPosition.scrollBy(delta);
+        requestLayout();
+    }
+
+    private TargetPosition getCurrentPosition() {
+        if (cellListManager.getLazyCellList().getMemoizedCount() == 0) {
+            return TargetPosition.BEGINNING;
+        } else {
+            C cell = positioner.getVisibleCell(firstVisibleIndex);
+            return new StartOffStart(firstVisibleIndex, orientation.minY(cell));
+        }
+    }
+
+    private void itemsChanged(QuasiListChange<?> ch) {
+        for (QuasiListModification<?> mod : ch) {
+            targetPosition = targetPosition.transformByChange(
+                    mod.getFrom(), mod.getRemovedSize(), mod.getAddedSize());
+        }
+        requestLayout(); // TODO: could optimize to only request layout if
+        // target position changed or cells in the viewport
+        // are affected
+    }
+
+    void showLengthRegion(int itemIndex, double fromY, double toY) {
+        setTargetPosition(new MinDistanceTo(
+                itemIndex, Offset.fromStart(fromY), Offset.fromStart(toY)));
+    }
+
+    @Override
+    public void visit(StartOffStart targetPosition) {
+        cropToNeighborhoodOf(targetPosition.itemIndex);  // Fix for issue #70 (!)
+        positioner.placeStartAt(targetPosition.itemIndex, targetPosition.offsetFromStart);
+        fillViewportFrom(targetPosition.itemIndex);
+    }
+
+    @Override
+    public void visit(EndOffEnd targetPosition) {
+        cropToNeighborhoodOf(targetPosition.itemIndex);  // Related to issue #70 (?)
+        positioner.placeEndFromEnd(targetPosition.itemIndex, targetPosition.offsetFromEnd);
+        fillViewportFrom(targetPosition.itemIndex);
+    }
+
+    private void cropToNeighborhoodOf(int itemIndex) {
+        int begin = Math.max(0, getFirstVisibleIndex());
+        int end = Math.max(itemIndex, getLastVisibleIndex());
+        positioner.cropTo(Math.min(begin, itemIndex), end + 1);
+    }
+
+    @Override
+    public void visit(MinDistanceTo targetPosition) {
+        Optional<C> cell = positioner.getCellIfVisible(targetPosition.itemIndex);
+        if (cell.isPresent()) {
+            placeToViewport(targetPosition.itemIndex, targetPosition.minY, targetPosition.maxY);
+        } else {
+            OptionalInt prevVisible;
+            OptionalInt nextVisible;
+            if ((prevVisible = positioner.lastVisibleBefore(targetPosition.itemIndex)).isPresent()) {
+                // Try keeping prevVisible in place:
+                // fill the viewport, see if the target item appeared.
+                fillForwardFrom(prevVisible.getAsInt());
+                cell = positioner.getCellIfVisible(targetPosition.itemIndex);
+                if (cell.isPresent()) {
+                    placeToViewport(targetPosition.itemIndex, targetPosition.minY, targetPosition.maxY);
+                } else if (targetPosition.maxY.isFromStart()) {
+                    placeStartOffEndMayCrop(targetPosition.itemIndex, -targetPosition.maxY.getValue());
+                } else {
+                    placeEndOffEndMayCrop(targetPosition.itemIndex, -targetPosition.maxY.getValue());
+                }
+            } else if ((nextVisible = positioner.firstVisibleAfter(targetPosition.itemIndex + 1)).isPresent()) {
+                // Try keeping nextVisible in place:
+                // fill the viewport, see if the target item appeared.
+                fillBackwardFrom(nextVisible.getAsInt());
+                cell = positioner.getCellIfVisible(targetPosition.itemIndex);
+                if (cell.isPresent()) {
+                    placeToViewport(targetPosition.itemIndex, targetPosition.minY, targetPosition.maxY);
+                } else if (targetPosition.minY.isFromStart()) {
+                    placeStartAtMayCrop(targetPosition.itemIndex, -targetPosition.minY.getValue());
+                } else {
+                    placeEndOffStartMayCrop(targetPosition.itemIndex, -targetPosition.minY.getValue());
+                }
+            } else {
+                if (targetPosition.minY.isFromStart()) {
+                    placeStartAtMayCrop(targetPosition.itemIndex, -targetPosition.minY.getValue());
+                } else {
+                    placeEndOffStartMayCrop(targetPosition.itemIndex, -targetPosition.minY.getValue());
+                }
+            }
+        }
+        fillViewportFrom(targetPosition.itemIndex);
+    }
+
+    /**
+     * Get the index of the first visible cell (at the time of the last layout).
+     *
+     * @return The index of the first visible cell
+     */
+    public int getFirstVisibleIndex() {
+        return firstVisibleIndex;
+    }
+
+    /**
+     * Get the index of the last visible cell (at the time of the last layout).
+     *
+     * @return The index of the last visible cell
+     */
+    public int getLastVisibleIndex() {
+        return lastVisibleIndex;
+    }
+
+    private void placeToViewport(int itemIndex, Offset from, Offset to) {
+        C cell = positioner.getVisibleCell(itemIndex);
+        double fromY = from.isFromStart()
+                ? from.getValue()
+                : orientation.length(cell) + to.getValue();
+        double toY = to.isFromStart()
+                ? to.getValue()
+                : orientation.length(cell) + to.getValue();
+        placeToViewport(itemIndex, fromY, toY);
+    }
+
+    private void placeToViewport(int itemIndex, double fromY, double toY) {
+        C cell = positioner.getVisibleCell(itemIndex);
+        double d = positioner.shortestDeltaToViewport(cell, fromY, toY);
+        positioner.placeStartAt(itemIndex, orientation.minY(cell) + d);
+    }
+
+    private void placeStartAtMayCrop(int itemIndex, double startOffStart) {
+        cropToNeighborhoodOf(itemIndex, startOffStart);
+        positioner.placeStartAt(itemIndex, startOffStart);
+    }
+
+    private void placeStartOffEndMayCrop(int itemIndex, double startOffEnd) {
+        cropToNeighborhoodOf(itemIndex, startOffEnd);
+        positioner.placeStartFromEnd(itemIndex, startOffEnd);
+    }
+
+    private void placeEndOffStartMayCrop(int itemIndex, double endOffStart) {
+        cropToNeighborhoodOf(itemIndex, endOffStart);
+        positioner.placeEndFromStart(itemIndex, endOffStart);
+    }
+
+    private void placeEndOffEndMayCrop(int itemIndex, double endOffEnd) {
+        cropToNeighborhoodOf(itemIndex, endOffEnd);
+        positioner.placeEndFromEnd(itemIndex, endOffEnd);
+    }
+
+    private void cropToNeighborhoodOf(int itemIndex, double additionalOffset) {
+        double spaceBefore = Math.max(0, sizeTracker.getViewportLength() + additionalOffset);
+        double spaceAfter = Math.max(0, sizeTracker.getViewportLength() - additionalOffset);
+
+        Optional<Double> avgLen = sizeTracker.getAverageLengthEstimate();
+        int itemsBefore = avgLen.map(l -> spaceBefore / l).orElse(5.0).intValue();
+        int itemsAfter = avgLen.map(l -> spaceAfter / l).orElse(5.0).intValue();
+
+        positioner.cropTo(itemIndex - itemsBefore, itemIndex + 1 + itemsAfter);
+    }
+
+    private int fillForwardFrom(int itemIndex) {
+        return fillForwardFrom(itemIndex, sizeTracker.getViewportLength());
+    }
+
+    private int fillForwardFrom0(int itemIndex) {
+        return fillForwardFrom0(itemIndex, sizeTracker.getViewportLength());
+    }
+
+    private int fillForwardFrom(int itemIndex, double upTo) {
+        // resize and/or reposition the starting cell
+        // in case the preferred or available size changed
+        C cell = positioner.getVisibleCell(itemIndex);
+        double length0 = orientation.minY(cell);
+        positioner.placeStartAt(itemIndex, length0);
+
+        return fillForwardFrom0(itemIndex, upTo);
+    }
+
+    int fillForwardFrom0(int itemIndex, double upTo) {
+        double max = orientation.maxY(positioner.getVisibleCell(itemIndex));
+        int i = itemIndex;
+        while (max < upTo && i < cellListManager.getLazyCellList().size() - 1) {
+            ++i;
+            C c = positioner.placeStartAt(i, max);
+            max = orientation.maxY(c);
+        }
+        return i;
+    }
+
+    private int fillBackwardFrom(int itemIndex) {
+        return fillBackwardFrom(itemIndex, 0.0);
+    }
+
+    private int fillBackwardFrom0(int itemIndex) {
+        return fillBackwardFrom0(itemIndex, 0.0);
+    }
+
+    private int fillBackwardFrom(int itemIndex, double upTo) {
+        // resize and/or reposition the starting cell
+        // in case the preferred or available size changed
+        C cell = positioner.getVisibleCell(itemIndex);
+        double length0 = orientation.minY(cell);
+        positioner.placeStartAt(itemIndex, length0);
+
+        return fillBackwardFrom0(itemIndex, upTo);
+    }
+
+    // does not re-place the anchor cell
+    int fillBackwardFrom0(int itemIndex, double upTo) {
+        double min = orientation.minY(positioner.getVisibleCell(itemIndex));
+        int i = itemIndex;
+        while (min > upTo && i > 0) {
+            --i;
+            C c = positioner.placeEndFromStart(i, min);
+            min = orientation.minY(c);
+        }
+        return i;
+    }
+
+    /**
+     * Starting from the anchor cell's node, fills the viewport from the anchor to the "ground" and then from the anchor
+     * to the "sky".
+     *
+     * @param itemIndex the index of the anchor cell
+     */
+    private void fillViewportFrom(int itemIndex) {
+        // cell for itemIndex is assumed to be placed correctly
+
+        // fill up to the ground
+        int ground = fillTowardsGroundFrom0(itemIndex);
+
+        // if ground not reached, shift cells to the ground
+        double gapBefore = distanceFromGround(ground);
+        if (gapBefore > 0) {
+            shiftCellsTowardsGround(ground, itemIndex, gapBefore);
+        }
+
+        // fill up to the sky
+        int sky = fillTowardsSkyFrom0(itemIndex);
+
+        // if sky not reached, add more cells under the ground and then shift
+        double gapAfter = distanceFromSky(sky);
+        if (gapAfter > 0) {
+            ground = fillTowardsGroundFrom0(ground, -gapAfter);
+            double extraBefore = -distanceFromGround(ground);
+            double shift = Math.min(gapAfter, extraBefore);
+            shiftCellsTowardsGround(ground, sky, -shift);
+        }
+
+        // crop to the visible cells
+        int first = Math.min(ground, sky);
+        int last = Math.max(ground, sky);
+        while (first < last &&
+                orientation.maxY(positioner.getVisibleCell(first)) <= 0.0) {
+            ++first;
+        }
+        while (last > first &&
+                orientation.minY(positioner.getVisibleCell(last)) >= sizeTracker.getViewportLength()) {
+            --last;
+        }
+        firstVisibleIndex = first;
+        lastVisibleIndex = last;
+        positioner.cropTo(first, last + 1);
+    }
+
+    private int fillTowardsGroundFrom0(int itemIndex) {
+        return gravity.get() == Gravity.FRONT
+                ? fillBackwardFrom0(itemIndex)
+                : fillForwardFrom0(itemIndex);
+    }
+
+    private int fillTowardsGroundFrom0(int itemIndex, double upTo) {
+        return gravity.get() == Gravity.FRONT
+                ? fillBackwardFrom0(itemIndex, upTo)
+                : fillForwardFrom0(itemIndex, sizeTracker.getViewportLength() - upTo);
+    }
+
+    private int fillTowardsSkyFrom0(int itemIndex) {
+        return gravity.get() == Gravity.FRONT
+                ? fillForwardFrom0(itemIndex)
+                : fillBackwardFrom0(itemIndex);
+    }
+
+    private double distanceFromGround(int itemIndex) {
+        C cell = positioner.getVisibleCell(itemIndex);
+        return gravity.get() == Gravity.FRONT
+                ? orientation.minY(cell)
+                : sizeTracker.getViewportLength() - orientation.maxY(cell);
+    }
+
+    private double distanceFromSky(int itemIndex) {
+        C cell = positioner.getVisibleCell(itemIndex);
+        return gravity.get() == Gravity.FRONT
+                ? sizeTracker.getViewportLength() - orientation.maxY(cell)
+                : orientation.minY(cell);
+    }
+
+    private void shiftCellsTowardsGround(
+            int groundCellIndex, int lastCellIndex, double amount) {
+        if (gravity.get() == Gravity.FRONT) {
+            assert groundCellIndex <= lastCellIndex;
+            for (int i = groundCellIndex; i <= lastCellIndex; ++i) {
+                positioner.shiftCellBy(positioner.getVisibleCell(i), -amount);
+            }
+        } else {
+            assert groundCellIndex >= lastCellIndex;
+            for (int i = groundCellIndex; i >= lastCellIndex; --i) {
+                positioner.shiftCellBy(positioner.getVisibleCell(i), amount);
+            }
+        }
+    }
+}

+ 475 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/OrientationHelper.java

@@ -0,0 +1,475 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.beans.property.DoubleProperty;
+import javafx.geometry.Bounds;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import org.reactfx.value.Val;
+import org.reactfx.value.Var;
+
+/**
+ * Helper class for returning the correct value (should the {@code width} or {@code height} be returned?) or calling
+ * the correct method (should {@code setWidth(args)} or {@code setHeight(args)}, so that one one class can be used
+ * instead of a generic with two implementations. See its implementations for more details ({@link VerticalHelper}
+ * and {@link HorizontalHelper}) on what "layoutX", "layoutY", and "viewport offset" values represent.
+ */
+interface OrientationHelper {
+    Orientation getContentBias();
+
+    double getX(double x, double y);
+
+    double getY(double x, double y);
+
+    double length(Bounds bounds);
+
+    double breadth(Bounds bounds);
+
+    double minX(Bounds bounds);
+
+    double minY(Bounds bounds);
+
+    default double maxX(Bounds bounds) {
+        return minX(bounds) + breadth(bounds);
+    }
+
+    default double maxY(Bounds bounds) {
+        return minY(bounds) + length(bounds);
+    }
+
+    double layoutX(Node node);
+
+    double layoutY(Node node);
+
+    DoubleProperty layoutYProperty(Node node);
+
+    default double length(Node node) {
+        return length(node.getLayoutBounds());
+    }
+
+    default double breadth(Node node) {
+        return breadth(node.getLayoutBounds());
+    }
+
+    default Val<Double> minYProperty(Node node) {
+        return Val.combine(
+                layoutYProperty(node),
+                node.layoutBoundsProperty(),
+                (layoutY, layoutBounds) -> layoutY.doubleValue() + minY(layoutBounds));
+    }
+
+    default double minY(Node node) {
+        return layoutY(node) + minY(node.getLayoutBounds());
+    }
+
+    default double maxY(Node node) {
+        return minY(node) + length(node);
+    }
+
+    default double minX(Node node) {
+        return layoutX(node) + minX(node.getLayoutBounds());
+    }
+
+    default double maxX(Node node) {
+        return minX(node) + breadth(node);
+    }
+
+    default double length(Cell<?, ?> cell) {
+        return length(cell.getNode());
+    }
+
+    default double breadth(Cell<?, ?> cell) {
+        return breadth(cell.getNode());
+    }
+
+    default Val<Double> minYProperty(Cell<?, ?> cell) {
+        return minYProperty(cell.getNode());
+    }
+
+    default double minY(Cell<?, ?> cell) {
+        return minY(cell.getNode());
+    }
+
+    default double maxY(Cell<?, ?> cell) {
+        return maxY(cell.getNode());
+    }
+
+    default double minX(Cell<?, ?> cell) {
+        return minX(cell.getNode());
+    }
+
+    default double maxX(Cell<?, ?> cell) {
+        return maxX(cell.getNode());
+    }
+
+    double minBreadth(Node node);
+
+    default double minBreadth(Cell<?, ?> cell) {
+        return minBreadth(cell.getNode());
+    }
+
+    double prefBreadth(Node node);
+
+    double prefLength(Node node, double breadth);
+
+    default double prefLength(Cell<?, ?> cell, double breadth) {
+        return prefLength(cell.getNode(), breadth);
+    }
+
+    void resizeRelocate(Node node, double b0, double l0, double breadth, double length);
+
+    void resize(Node node, double breadth, double length);
+
+    void relocate(Node node, double b0, double l0);
+
+    default void resize(Cell<?, ?> cell, double breadth, double length) {
+        resize(cell.getNode(), breadth, length);
+    }
+
+    default void relocate(Cell<?, ?> cell, double b0, double l0) {
+        relocate(cell.getNode(), b0, l0);
+    }
+
+    Val<Double> widthEstimateProperty(VirtualFlow<?, ?> content);
+
+    Val<Double> heightEstimateProperty(VirtualFlow<?, ?> content);
+
+    Var<Double> estimatedScrollXProperty(VirtualFlow<?, ?> content);
+
+    Var<Double> estimatedScrollYProperty(VirtualFlow<?, ?> content);
+
+    void scrollHorizontallyBy(VirtualFlow<?, ?> content, double dx);
+
+    void scrollVerticallyBy(VirtualFlow<?, ?> content, double dy);
+
+    void scrollHorizontallyToPixel(VirtualFlow<?, ?> content, double pixel);
+
+    void scrollVerticallyToPixel(VirtualFlow<?, ?> content, double pixel);
+
+    <C extends Cell<?, ?>> VirtualFlowHit<C> hitBeforeCells(double bOff, double lOff);
+
+    <C extends Cell<?, ?>> VirtualFlowHit<C> hitAfterCells(double bOff, double lOff);
+
+    <C extends Cell<?, ?>> VirtualFlowHit<C> cellHit(int itemIndex, C cell, double bOff, double lOff);
+}
+
+/**
+ * Implementation of {@link OrientationHelper} where {@code length} represents width of the node/viewport and
+ * {@code breadth} represents the height of the node/viewport. "layoutY" is {@link Node#getLayoutX()} and
+ * "layoutX" is {@link Node#getLayoutY()}. "viewport offset" values are based on width. The viewport's "top"
+ * and "bottom" edges are either it's left/right edges (See {@link VirtualFlow.Gravity}).
+ */
+final class HorizontalHelper implements OrientationHelper {
+
+    @Override
+    public Orientation getContentBias() {
+        return Orientation.VERTICAL;
+    }
+
+    @Override
+    public double getX(double x, double y) {
+        return y;
+    }
+
+    @Override
+    public double getY(double x, double y) {
+        return x;
+    }
+
+    @Override
+    public double minBreadth(Node node) {
+        return node.minHeight(-1);
+    }
+
+    @Override
+    public double prefBreadth(Node node) {
+        return node.prefHeight(-1);
+    }
+
+    @Override
+    public double prefLength(Node node, double breadth) {
+        return node.prefWidth(breadth);
+    }
+
+    @Override
+    public double breadth(Bounds bounds) {
+        return bounds.getHeight();
+    }
+
+    @Override
+    public double length(Bounds bounds) {
+        return bounds.getWidth();
+    }
+
+    @Override
+    public double minX(Bounds bounds) {
+        return bounds.getMinY();
+    }
+
+    @Override
+    public double minY(Bounds bounds) {
+        return bounds.getMinX();
+    }
+
+    @Override
+    public double layoutX(Node node) {
+        return node.getLayoutY();
+    }
+
+    @Override
+    public double layoutY(Node node) {
+        return node.getLayoutX();
+    }
+
+    @Override
+    public DoubleProperty layoutYProperty(Node node) {
+        return node.layoutXProperty();
+    }
+
+    @Override
+    public void resizeRelocate(
+            Node node, double b0, double l0, double breadth, double length) {
+        node.resizeRelocate(l0, b0, length, breadth);
+    }
+
+    @Override
+    public void resize(Node node, double breadth, double length) {
+        node.resize(length, breadth);
+    }
+
+    @Override
+    public void relocate(Node node, double b0, double l0) {
+        node.relocate(l0, b0);
+    }
+
+    @Override
+    public Val<Double> widthEstimateProperty(
+            VirtualFlow<?, ?> content) {
+        return content.totalLengthEstimateProperty();
+    }
+
+    @Override
+    public Val<Double> heightEstimateProperty(
+            VirtualFlow<?, ?> content) {
+        return content.totalBreadthEstimateProperty();
+    }
+
+    @Override
+    public Var<Double> estimatedScrollXProperty(
+            VirtualFlow<?, ?> content) {
+        return content.lengthOffsetEstimateProperty();
+    }
+
+    @Override
+    public Var<Double> estimatedScrollYProperty(
+            VirtualFlow<?, ?> content) {
+        return content.breadthOffsetProperty();
+    }
+
+    @Override
+    public void scrollHorizontallyBy(VirtualFlow<?, ?> content, double dx) {
+        content.scrollLength(dx);
+    }
+
+    @Override
+    public void scrollVerticallyBy(VirtualFlow<?, ?> content, double dy) {
+        content.scrollBreadth(dy);
+    }
+
+    @Override
+    public void scrollHorizontallyToPixel(VirtualFlow<?, ?> content, double pixel) {
+        content.setLengthOffset(pixel);
+    }
+
+    @Override
+    public void scrollVerticallyToPixel(VirtualFlow<?, ?> content, double pixel) {
+        content.setBreadthOffset(pixel);
+    }
+
+    @Override
+    public <C extends Cell<?, ?>> VirtualFlowHit<C> hitBeforeCells(
+            double bOff, double lOff) {
+        return VirtualFlowHit.hitBeforeCells(lOff, bOff);
+    }
+
+    @Override
+    public <C extends Cell<?, ?>> VirtualFlowHit<C> hitAfterCells(
+            double bOff, double lOff) {
+        return VirtualFlowHit.hitAfterCells(lOff, bOff);
+    }
+
+    @Override
+    public <C extends Cell<?, ?>> VirtualFlowHit<C> cellHit(
+            int itemIndex, C cell, double bOff, double lOff) {
+        return VirtualFlowHit.cellHit(itemIndex, cell, lOff, bOff);
+    }
+}
+
+/**
+ * Implementation of {@link OrientationHelper} where {@code breadth} represents width of the node/viewport and
+ * {@code length} represents the height of the node/viewport. "layoutX" is {@link Node#getLayoutX()} and
+ * "layoutY" is {@link Node#getLayoutY()}. "viewport offset" values are based on height. The viewport's "top"
+ * and "bottom" edges are either it's top/bottom edges (See {@link VirtualFlow.Gravity}).
+ */
+final class VerticalHelper implements OrientationHelper {
+
+    @Override
+    public Orientation getContentBias() {
+        return Orientation.HORIZONTAL;
+    }
+
+    @Override
+    public double getX(double x, double y) {
+        return x;
+    }
+
+    @Override
+    public double getY(double x, double y) {
+        return y;
+    }
+
+    @Override
+    public double minBreadth(Node node) {
+        return node.minWidth(-1);
+    }
+
+    @Override
+    public double prefBreadth(Node node) {
+        return node.prefWidth(-1);
+    }
+
+    @Override
+    public double prefLength(Node node, double breadth) {
+        return node.prefHeight(breadth);
+    }
+
+    @Override
+    public double breadth(Bounds bounds) {
+        return bounds.getWidth();
+    }
+
+    @Override
+    public double length(Bounds bounds) {
+        return bounds.getHeight();
+    }
+
+    @Override
+    public double minX(Bounds bounds) {
+        return bounds.getMinX();
+    }
+
+    @Override
+    public double minY(Bounds bounds) {
+        return bounds.getMinY();
+    }
+
+    @Override
+    public double layoutX(Node node) {
+        return node.getLayoutX();
+    }
+
+    @Override
+    public double layoutY(Node node) {
+        return node.getLayoutY();
+    }
+
+    @Override
+    public DoubleProperty layoutYProperty(Node node) {
+        return node.layoutYProperty();
+    }
+
+    @Override
+    public void resizeRelocate(
+            Node node, double b0, double l0, double breadth, double length) {
+        node.resizeRelocate(b0, l0, breadth, length);
+    }
+
+    @Override
+    public void resize(Node node, double breadth, double length) {
+        node.resize(breadth, length);
+    }
+
+    @Override
+    public void relocate(Node node, double b0, double l0) {
+        node.relocate(b0, l0);
+    }
+
+    @Override
+    public Val<Double> widthEstimateProperty(
+            VirtualFlow<?, ?> content) {
+        return content.totalBreadthEstimateProperty();
+    }
+
+    @Override
+    public Val<Double> heightEstimateProperty(
+            VirtualFlow<?, ?> content) {
+        return content.totalLengthEstimateProperty();
+    }
+
+    @Override
+    public Var<Double> estimatedScrollXProperty(
+            VirtualFlow<?, ?> content) {
+        return content.breadthOffsetProperty();
+    }
+
+    @Override
+    public Var<Double> estimatedScrollYProperty(
+            VirtualFlow<?, ?> content) {
+        return content.lengthOffsetEstimateProperty();
+    }
+
+    @Override
+    public void scrollHorizontallyBy(VirtualFlow<?, ?> content, double dx) {
+        content.scrollBreadth(dx);
+    }
+
+    @Override
+    public void scrollVerticallyBy(VirtualFlow<?, ?> content, double dy) {
+        content.scrollLength(dy);
+    }
+
+    @Override
+    public void scrollHorizontallyToPixel(VirtualFlow<?, ?> content, double pixel) {
+        content.setBreadthOffset(pixel);
+    }
+
+    @Override
+    public void scrollVerticallyToPixel(VirtualFlow<?, ?> content, double pixel) { // length
+        content.setLengthOffset(pixel);
+    }
+
+    @Override
+    public <C extends Cell<?, ?>> VirtualFlowHit<C> hitBeforeCells(
+            double bOff, double lOff) {
+        return VirtualFlowHit.hitBeforeCells(bOff, lOff);
+    }
+
+    @Override
+    public <C extends Cell<?, ?>> VirtualFlowHit<C> hitAfterCells(
+            double bOff, double lOff) {
+        return VirtualFlowHit.hitAfterCells(bOff, lOff);
+    }
+
+    @Override
+    public <C extends Cell<?, ?>> VirtualFlowHit<C> cellHit(
+            int itemIndex, C cell, double bOff, double lOff) {
+        return VirtualFlowHit.cellHit(itemIndex, cell, bOff, lOff);
+    }
+}

+ 144 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/ScaledVirtualized.java

@@ -0,0 +1,144 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.scene.Node;
+import javafx.scene.layout.Region;
+import javafx.scene.transform.Scale;
+import org.reactfx.value.Val;
+import org.reactfx.value.Var;
+
+/**
+ * Acts as an intermediate class between {@link VirtualizedScrollPane} and
+ * its {@link Virtualized} content in that it scales the content without
+ * also scaling the ScrollPane's scroll bars.
+ * <pre>
+ *     {@code
+ *     Virtualized actualContent = // creation code
+ *     ScaledVirtualized<Virtualized> wrapper = new ScaledVirtualized(actualContent);
+ *     VirtualizedScrollPane<ScaledVirtualized> vsPane = new VirtualizedScrollPane(wrapper);
+ *
+ *     // To scale actualContent without also scaling vsPane's scrollbars:
+ *     wrapper.scaleProperty().setY(3);
+ *     wrapper.scaleProperty().setX(2);
+ *     }
+ * </pre>
+ *
+ * @param <V> the {@link Virtualized} content to be scaled when inside a {@link VirtualizedScrollPane}
+ */
+public class ScaledVirtualized<V extends Node & Virtualized> extends Region implements Virtualized {
+    private final V content;
+    private final Scale zoom = new Scale();
+
+    private final Val<Double> estHeight;
+    private final Val<Double> estWidth;
+    private final Var<Double> estScrollX;
+    private final Var<Double> estScrollY;
+
+    public ScaledVirtualized(V content) {
+        super();
+        this.content = content;
+        getChildren().add(content);
+        getTransforms().add(zoom);
+
+        estHeight = Val.combine(
+                content.totalHeightEstimateProperty(),
+                zoom.yProperty(),
+                (estHeight, scaleFactor) -> estHeight * scaleFactor.doubleValue()
+        );
+        estWidth = Val.combine(
+                content.totalWidthEstimateProperty(),
+                zoom.xProperty(),
+                (estWidth, scaleFactor) -> estWidth * scaleFactor.doubleValue()
+        );
+        estScrollX = Var.mapBidirectional(
+                content.estimatedScrollXProperty(),
+                scrollX -> scrollX * zoom.getX(),
+                scrollX -> scrollX / zoom.getX()
+        );
+        estScrollY = Var.mapBidirectional(
+                content.estimatedScrollYProperty(),
+                scrollY -> scrollY * zoom.getY(),
+                scrollY -> scrollY / zoom.getY()
+        );
+
+        zoom.xProperty().addListener((obs, ov, nv) -> requestLayout());
+        zoom.yProperty().addListener((obs, ov, nv) -> requestLayout());
+        zoom.zProperty().addListener((obs, ov, nv) -> requestLayout());
+        zoom.pivotXProperty().addListener((obs, ov, nv) -> requestLayout());
+        zoom.pivotYProperty().addListener((obs, ov, nv) -> requestLayout());
+        zoom.pivotZProperty().addListener((obs, ov, nv) -> requestLayout());
+    }
+
+    @Override
+    protected void layoutChildren() {
+        double width = getLayoutBounds().getWidth();
+        double height = getLayoutBounds().getHeight();
+        content.resize(width / zoom.getX(), height / zoom.getY());
+    }
+
+    @Override
+    public Var<Double> estimatedScrollXProperty() {
+        return estScrollX;
+    }
+
+    @Override
+    public Var<Double> estimatedScrollYProperty() {
+        return estScrollY;
+    }
+
+    @Override
+    public Val<Double> totalHeightEstimateProperty() {
+        return estHeight;
+    }
+
+    @Override
+    public Val<Double> totalWidthEstimateProperty() {
+        return estWidth;
+    }
+
+    @Override
+    public void scrollXBy(double deltaX) {
+        content.scrollXBy(deltaX);
+    }
+
+    @Override
+    public void scrollYBy(double deltaY) {
+        content.scrollYBy(deltaY);
+    }
+
+    @Override
+    public void scrollXToPixel(double pixel) {
+        content.scrollXToPixel(pixel);
+    }
+
+    @Override
+    public void scrollYToPixel(double pixel) {
+        content.scrollYToPixel(pixel);
+    }
+
+    /**
+     * The {@link Scale} object that scales the virtualized content: named "zoom"
+     * to prevent confusion with {@link Node#getScaleX()}, etc. Not to be confused
+     * with {@link Node#getOnZoom()} or similar methods/objects.
+     */
+    public Scale getZoom() {
+        return zoom;
+    }
+}

+ 222 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/SizeTracker.java

@@ -0,0 +1,222 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.beans.value.ObservableObjectValue;
+import javafx.geometry.Bounds;
+import javafx.scene.control.IndexRange;
+import org.reactfx.EventStreams;
+import org.reactfx.Subscription;
+import org.reactfx.collection.LiveList;
+import org.reactfx.collection.MemoizationList;
+import org.reactfx.value.Val;
+import org.reactfx.value.ValBase;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Estimates the size of the entire viewport (if it was actually completely rendered) based on the known sizes of the
+ * {@link Cell}s whose nodes are currently displayed in the viewport and an estimated average of
+ * {@link Cell}s whose nodes are not displayed in the viewport. The meaning of {@link #breadthForCells} and
+ * {@link #totalLengthEstimate} are dependent upon which implementation of {@link OrientationHelper} is used.
+ */
+final class SizeTracker {
+    private final OrientationHelper orientation;
+    private final ObservableObjectValue<Bounds> viewportBounds;
+    private final MemoizationList<? extends Cell<?, ?>> cells;
+
+    private final MemoizationList<Double> breadths;
+    private final Val<Double> maxKnownMinBreadth;
+
+    /**
+     * Stores either the greatest minimum cell's node's breadth or the viewport's breadth
+     */
+    private final Val<Double> breadthForCells;
+
+    private final MemoizationList<Double> lengths;
+
+    /**
+     * Stores either null or the average length of the cells' nodes currently displayed in the viewport
+     */
+    private final Val<Double> averageLengthEstimate;
+
+    private final Val<Double> totalLengthEstimate;
+    private final Val<Double> lengthOffsetEstimate;
+
+    private final Subscription subscription;
+
+    /**
+     * Constructs a SizeTracker
+     *
+     * @param orientation if vertical, breadth = width and length = height;
+     *                    if horizontal, breadth = height and length = width
+     */
+    public SizeTracker(
+            OrientationHelper orientation,
+            ObservableObjectValue<Bounds> viewportBounds,
+            MemoizationList<? extends Cell<?, ?>> lazyCells) {
+        this.orientation = orientation;
+        this.viewportBounds = viewportBounds;
+        this.cells = lazyCells;
+        this.breadths = lazyCells.map(orientation::minBreadth).memoize();
+        this.maxKnownMinBreadth = breadths.memoizedItems()
+                .reduce(Math::max)
+                .orElseConst(0.0);
+        this.breadthForCells = Val.combine(
+                maxKnownMinBreadth,
+                viewportBounds,
+                (a, b) -> Math.max(a, orientation.breadth(b)));
+
+        Val<Function<Cell<?, ?>, Double>> lengthFn;
+        lengthFn = (orientation instanceof HorizontalHelper ? breadthForCells : avoidFalseInvalidations(breadthForCells))
+                .map(breadth -> cell -> orientation.prefLength(cell, breadth));
+
+        this.lengths = cells.mapDynamic(lengthFn).memoize();
+
+        LiveList<Double> knownLengths = this.lengths.memoizedItems();
+        Val<Double> sumOfKnownLengths = knownLengths.reduce(Double::sum).orElseConst(0.0);
+        Val<Integer> knownLengthCount = knownLengths.sizeProperty();
+
+        this.averageLengthEstimate = Val.create(
+                () -> {
+                    // make sure to use pref lengths of all present cells
+                    for (int i = 0; i < cells.getMemoizedCount(); ++i) {
+                        int j = cells.indexOfMemoizedItem(i);
+                        lengths.force(j, j + 1);
+                    }
+
+                    int count = knownLengthCount.getValue();
+                    return count == 0
+                            ? null
+                            : sumOfKnownLengths.getValue() / count;
+                },
+                sumOfKnownLengths, knownLengthCount);
+
+        this.totalLengthEstimate = Val.combine(
+                averageLengthEstimate, cells.sizeProperty(),
+                (avg, n) -> n * avg);
+
+        Val<Integer> firstVisibleIndex = Val.create(
+                () -> cells.getMemoizedCount() == 0 ? null : cells.indexOfMemoizedItem(0),
+                cells, cells.memoizedItems()); // need to observe cells.memoizedItems()
+        // as well, because they may change without a change in cells.
+
+        Val<? extends Cell<?, ?>> firstVisibleCell = cells.memoizedItems()
+                .collapse(visCells -> visCells.isEmpty() ? null : visCells.get(0));
+
+        Val<Integer> knownLengthCountBeforeFirstVisibleCell = Val.create(() -> firstVisibleIndex.getOpt()
+                .map(i -> lengths.getMemoizedCountBefore(Math.min(i, lengths.size())))
+                .orElse(0), lengths, firstVisibleIndex);
+
+        Val<Double> totalKnownLengthBeforeFirstVisibleCell = knownLengths.reduceRange(
+                knownLengthCountBeforeFirstVisibleCell.map(n -> new IndexRange(0, n)),
+                Double::sum).orElseConst(0.0);
+
+        Val<Double> unknownLengthEstimateBeforeFirstVisibleCell = Val.combine(
+                firstVisibleIndex,
+                knownLengthCountBeforeFirstVisibleCell,
+                averageLengthEstimate,
+                (firstIdx, knownCnt, avgLen) -> (firstIdx - knownCnt) * avgLen);
+
+        Val<Double> firstCellMinY = firstVisibleCell.flatMap(orientation::minYProperty);
+
+        lengthOffsetEstimate = Val.wrap(EventStreams.combine(
+                totalKnownLengthBeforeFirstVisibleCell.values(),
+                unknownLengthEstimateBeforeFirstVisibleCell.values(),
+                firstCellMinY.values()
+        )
+                .filter(t3 -> t3.test((a, b, minY) -> a != null && b != null && minY != null))
+                .thenRetainLatestFor(Duration.ofMillis(1))
+                .map(t3 -> t3.map((a, b, minY) -> a + b - minY))
+                .toBinding(0.0));
+
+        // pinning totalLengthEstimate and lengthOffsetEstimate
+        // binds it all together and enables memoization
+        this.subscription = Subscription.multi(
+                totalLengthEstimate.pin(),
+                lengthOffsetEstimate.pin());
+    }
+
+    private static <T> Val<T> avoidFalseInvalidations(Val<T> src) {
+        return new ValBase<>() {
+            @Override
+            protected Subscription connect() {
+                return src.observeChanges((obs, oldVal, newVal) -> invalidate());
+            }
+
+            @Override
+            protected T computeValue() {
+                return src.getValue();
+            }
+        };
+    }
+
+    public void dispose() {
+        subscription.unsubscribe();
+    }
+
+    public Val<Double> maxCellBreadthProperty() {
+        return maxKnownMinBreadth;
+    }
+
+    public double getViewportBreadth() {
+        return orientation.breadth(viewportBounds.get());
+    }
+
+    public double getViewportLength() {
+        return orientation.length(viewportBounds.get());
+    }
+
+    public Val<Double> averageLengthEstimateProperty() {
+        return averageLengthEstimate;
+    }
+
+    public Optional<Double> getAverageLengthEstimate() {
+        return averageLengthEstimate.getOpt();
+    }
+
+    public Val<Double> totalLengthEstimateProperty() {
+        return totalLengthEstimate;
+    }
+
+    public Val<Double> lengthOffsetEstimateProperty() {
+        return lengthOffsetEstimate;
+    }
+
+    public double breadthFor(int itemIndex) {
+        assert cells.isMemoized(itemIndex);
+        breadths.force(itemIndex, itemIndex + 1);
+        return breadthForCells.getValue();
+    }
+
+    public void forgetSizeOf(int itemIndex) {
+        breadths.forget(itemIndex, itemIndex + 1);
+        lengths.forget(itemIndex, itemIndex + 1);
+    }
+
+    public double lengthFor(int itemIndex) {
+        return lengths.get(itemIndex);
+    }
+
+    public double getCellLayoutBreadth() {
+        return breadthForCells.getValue();
+    }
+}

+ 102 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/StableBidirectionalVar.java

@@ -0,0 +1,102 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.application.Platform;
+import javafx.beans.property.Property;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import org.reactfx.Subscription;
+import org.reactfx.value.ProxyVal;
+import org.reactfx.value.Val;
+import org.reactfx.value.Var;
+
+import java.util.function.Consumer;
+
+/**
+ * This class overrides the <code>Var.bindBidirectional</code> method implementing a mechanism to prevent looping.
+ * <br>By default <code>bindBidirectional</code> delegates to <code>Bindings.bindBidirectional</code>
+ * which isn't always stable for the Val -> Var paradigm, sometimes producing a continuous feedback loop.<br>
+ */
+class StableBidirectionalVar<T> extends ProxyVal<T, T> implements Var<T> {
+    private final Consumer<T> setVal;
+    private Subscription binding = null;
+    private ChangeListener<T> left, right;
+    private T last = null;
+
+    StableBidirectionalVar(Val<T> underlying, Consumer<T> setter) {
+        super(underlying);
+        setVal = setter;
+    }
+
+    @Override
+    public T getValue() {
+        return getUnderlyingObservable().getValue();
+    }
+
+    @Override
+    protected Consumer<? super T> adaptObserver(Consumer<? super T> observer) {
+        return observer; // no adaptation needed
+    }
+
+    @Override
+    public void bind(ObservableValue<? extends T> observable) {
+        unbind();
+        binding = Val.observeChanges(observable, (ob, ov, nv) -> setValue(nv));
+        setValue(observable.getValue());
+    }
+
+    @Override
+    public boolean isBound() {
+        return binding != null;
+    }
+
+    @Override
+    public void unbind() {
+        if (binding != null) binding.unsubscribe();
+        binding = null;
+    }
+
+    @Override
+    public void setValue(T newVal) {
+        setVal.accept(newVal);
+    }
+
+    @Override
+    public void unbindBidirectional(Property<T> prop) {
+        if (right != null) prop.removeListener(right);
+        if (left != null) removeListener(left);
+        left = null;
+        right = null;
+    }
+
+    @Override
+    public void bindBidirectional(Property<T> prop) {
+        unbindBidirectional(prop);
+        prop.addListener(right = (ob, ov, nv) -> adjustOther(this, nv));
+        addListener(left = (ob, ov, nv) -> adjustOther(prop, nv));
+    }
+
+    private void adjustOther(Property<T> other, T nv) {
+        if (!nv.equals(last)) {
+            Platform.runLater(() -> other.setValue(nv));
+            last = nv;
+        }
+    }
+}

+ 263 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/TargetPosition.java

@@ -0,0 +1,263 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+/**
+ * Defines where the {@link Navigator} should place the anchor cell's node in the viewport. Its three implementations
+ * are {@link StartOffStart}, {@link EndOffEnd}, and {@link MinDistanceTo}.
+ */
+interface TargetPosition {
+    TargetPosition BEGINNING = new StartOffStart(0, 0.0);
+
+    /**
+     * When the list of items, those displayed in the viewport, and those that are not, are modified, transforms
+     * this change to account for those modifications.
+     *
+     * @param pos         the cell index where the change begins
+     * @param removedSize the amount of cells that were removed, starting from {@code pos}
+     * @param addedSize   the amount of cells that were added, starting from {@code pos}
+     */
+    TargetPosition transformByChange(int pos, int removedSize, int addedSize);
+
+
+    TargetPosition scrollBy(double delta);
+
+    /**
+     * Visitor Pattern: prevents type-checking the implementation
+     */
+    void accept(TargetPositionVisitor visitor);
+
+    /**
+     * Insures this position's item index is between 0 and {@code size}
+     */
+    TargetPosition clamp(int size);
+}
+
+/**
+ * Uses the Visitor Pattern, so {@link Navigator} does not need to check the type of the {@link TargetPosition}
+ * before using it to determine how to fill the viewport.
+ */
+interface TargetPositionVisitor {
+
+    void visit(StartOffStart targetPosition);
+
+    void visit(EndOffEnd targetPosition);
+
+    void visit(MinDistanceTo targetPosition);
+}
+
+/**
+ * A {@link TargetPosition} that instructs its {@link TargetPositionVisitor} to use the cell at {@link #itemIndex}
+ * as the anchor cell, showing it at the "top" of the viewport and to offset it by {@link #offsetFromStart}.
+ */
+final class StartOffStart implements TargetPosition {
+    final int itemIndex;
+    final double offsetFromStart;
+
+    StartOffStart(int itemIndex, double offsetFromStart) {
+        this.itemIndex = itemIndex;
+        this.offsetFromStart = offsetFromStart;
+    }
+
+    @Override
+    public TargetPosition transformByChange(
+            int pos, int removedSize, int addedSize) {
+        if (itemIndex >= pos + removedSize) {
+            // change before the target item, just update item index
+            return new StartOffStart(itemIndex - removedSize + addedSize, offsetFromStart);
+        } else if (itemIndex >= pos) {
+            // target item deleted
+            if (addedSize == removedSize) {
+                return this;
+            } else {
+                // show the first inserted at the target offset
+                return new StartOffStart(pos, offsetFromStart);
+            }
+        } else {
+            // change after the target item, target position not affected
+            return this;
+        }
+    }
+
+    @Override
+    public TargetPosition scrollBy(double delta) {
+        return new StartOffStart(itemIndex, offsetFromStart - delta);
+    }
+
+    @Override
+    public void accept(TargetPositionVisitor visitor) {
+        visitor.visit(this);
+    }
+
+    @Override
+    public TargetPosition clamp(int size) {
+        return new StartOffStart(clamp(itemIndex, size), offsetFromStart);
+    }
+
+    static int clamp(int idx, int size) {
+        if (size < 0) {
+            throw new IllegalArgumentException("size cannot be negative: " + size);
+        }
+        if (idx <= 0) {
+            return 0;
+        } else if (idx >= size) {
+            return size - 1;
+        } else {
+            return idx;
+        }
+    }
+}
+
+/**
+ * A {@link TargetPosition} that instructs its {@link TargetPositionVisitor} to use the cell at {@link #itemIndex}
+ * as the anchor cell, showing it at the "bottom" of the viewport and to offset it by {@link #offsetFromEnd}.
+ */
+final class EndOffEnd implements TargetPosition {
+    final int itemIndex;
+    final double offsetFromEnd;
+
+    EndOffEnd(int itemIndex, double offsetFromEnd) {
+        this.itemIndex = itemIndex;
+        this.offsetFromEnd = offsetFromEnd;
+    }
+
+    @Override
+    public TargetPosition transformByChange(
+            int pos, int removedSize, int addedSize) {
+        if (itemIndex >= pos + removedSize) {
+            // change before the target item, just update item index
+            return new EndOffEnd(itemIndex - removedSize + addedSize, offsetFromEnd);
+        } else if (itemIndex >= pos) {
+            // target item deleted
+            if (addedSize == removedSize) {
+                return this;
+            } else {
+                // show the last inserted at the target offset
+                return new EndOffEnd(pos + addedSize - 1, offsetFromEnd);
+            }
+        } else {
+            // change after the target item, target position not affected
+            return this;
+        }
+    }
+
+    @Override
+    public TargetPosition scrollBy(double delta) {
+        return new EndOffEnd(itemIndex, offsetFromEnd - delta);
+    }
+
+    @Override
+    public void accept(TargetPositionVisitor visitor) {
+        visitor.visit(this);
+    }
+
+    @Override
+    public TargetPosition clamp(int size) {
+        return new EndOffEnd(StartOffStart.clamp(itemIndex, size), offsetFromEnd);
+    }
+}
+
+final class MinDistanceTo implements TargetPosition {
+    final int itemIndex;
+    final Offset minY;
+    final Offset maxY;
+
+    MinDistanceTo(int itemIndex, Offset minY, Offset maxY) {
+        this.itemIndex = itemIndex;
+        this.minY = minY;
+        this.maxY = maxY;
+    }
+
+    public MinDistanceTo(int itemIndex) {
+        this(itemIndex, Offset.fromStart(0.0), Offset.fromEnd(0.0));
+    }
+
+    @Override
+    public TargetPosition transformByChange(
+            int pos, int removedSize, int addedSize) {
+        if (itemIndex >= pos + removedSize) {
+            // change before the target item, just update item index
+            return new MinDistanceTo(itemIndex - removedSize + addedSize, minY, maxY);
+        } else if (itemIndex >= pos) {
+            // target item deleted
+            if (addedSize == removedSize) {
+                return this;
+            } else {
+                // show the first inserted
+                return new MinDistanceTo(pos, Offset.fromStart(0.0), Offset.fromEnd(0.0));
+            }
+        } else {
+            // change after the target item, target position not affected
+            return this;
+        }
+    }
+
+    @Override
+    public TargetPosition scrollBy(double delta) {
+        return new MinDistanceTo(itemIndex, minY.add(delta), maxY.add(delta));
+    }
+
+    @Override
+    public void accept(TargetPositionVisitor visitor) {
+        visitor.visit(this);
+    }
+
+    @Override
+    public TargetPosition clamp(int size) {
+        return new MinDistanceTo(StartOffStart.clamp(itemIndex, size), minY, maxY);
+    }
+}
+
+/**
+ * Helper class: stores an {@link #offset} value, which should either be offset from the start if {@link #fromStart}
+ * is true or from the end if false.
+ */
+class Offset {
+    public static Offset fromStart(double offset) {
+        return new Offset(offset, true);
+    }
+
+    public static Offset fromEnd(double offset) {
+        return new Offset(offset, false);
+    }
+
+    private final double offset;
+    private final boolean fromStart;
+
+    private Offset(double offset, boolean fromStart) {
+        this.offset = offset;
+        this.fromStart = fromStart;
+    }
+
+    public double getValue() {
+        return offset;
+    }
+
+    public boolean isFromStart() {
+        return fromStart;
+    }
+
+    public boolean isFromEnd() {
+        return !fromStart;
+    }
+
+    public Offset add(double delta) {
+        return new Offset(offset + delta, fromStart);
+    }
+}

+ 646 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/VirtualFlow.java

@@ -0,0 +1,646 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.css.*;
+import javafx.geometry.Bounds;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.shape.Rectangle;
+import org.reactfx.collection.MemoizationList;
+import org.reactfx.util.Lists;
+import org.reactfx.value.Val;
+import org.reactfx.value.Var;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * A VirtualFlow is a memory-efficient viewport that only renders enough of its content to completely fill up the
+ * viewport through its {@link Navigator}. Based on the viewport's {@link Gravity}, it sequentially lays out the
+ * {@link javafx.scene.Node}s of the {@link Cell}s until the viewport is completely filled up or it has no additional
+ * cell's nodes to render.
+ *
+ * <p>
+ * Since this viewport does not fully render all of its content, the scroll values are estimates based on the nodes
+ * that are currently displayed in the viewport. If every node that could be rendered is the same width or same
+ * height, then the corresponding scroll values (e.g., scrollX or totalX) are accurate.
+ * <em>Note:</em> the VirtualFlow does not have scroll bars by default. These can be added by wrapping this object
+ * in a {@link VirtualizedScrollPane}.
+ * </p>
+ *
+ * <p>
+ * Since the viewport can be used to lay out its content horizontally or vertically, it uses two
+ * orientation-agnostic terms to refer to its width and height: "breadth" and "length," respectively. The viewport
+ * always lays out its {@link Cell cell}'s {@link javafx.scene.Node}s from "top-to-bottom" or from "bottom-to-top"
+ * (these terms should be understood in reference to the viewport's {@link OrientationHelper orientation} and
+ * {@link Gravity}). Thus, its length ("height") is independent as the viewport's bounds are dependent upon
+ * its parent's bounds whereas its breadth ("width") is dependent upon its length.
+ * </p>
+ *
+ * @param <T> the model content that the {@link Cell#getNode() cell's node} renders
+ * @param <C> the {@link Cell} that can render the model with a {@link javafx.scene.Node}.
+ */
+public class VirtualFlow<T, C extends Cell<T, ?>> extends Region implements Virtualized {
+
+    /**
+     * Determines how the cells in the viewport should be laid out and where any extra unused space should exist
+     * if there are not enough cells to completely fill up the viewport
+     */
+    public enum Gravity {
+        /**
+         * If using a {@link VerticalHelper vertical viewport}, lays out the content from top-to-bottom. The first
+         * visible item will appear at the top and the last visible item (or unused space) towards the bottom.
+         * <p>
+         * If using a {@link HorizontalHelper horizontal viewport}, lays out the content from left-to-right. The first
+         * visible item will appear at the left and the last visible item (or unused space) towards the right.
+         * </p>
+         */
+        FRONT,
+        /**
+         * If using a {@link VerticalHelper vertical viewport}, lays out the content from bottom-to-top. The first
+         * visible item will appear at the bottom and the last visible item (or unused space) towards the top.
+         * <p>
+         * If using a {@link HorizontalHelper horizontal viewport}, lays out the content from right-to-left. The first
+         * visible item will appear at the right and the last visible item (or unused space) towards the left.
+         * </p>
+         */
+        REAR
+    }
+
+    /**
+     * Creates a viewport that lays out content horizontally from left to right
+     */
+    public static <T, C extends Cell<T, ?>> VirtualFlow<T, C> createHorizontal(
+            ObservableList<T> items,
+            Function<? super T, ? extends C> cellFactory) {
+        return createHorizontal(items, cellFactory, Gravity.FRONT);
+    }
+
+    /**
+     * Creates a viewport that lays out content horizontally
+     */
+    public static <T, C extends Cell<T, ?>> VirtualFlow<T, C> createHorizontal(
+            ObservableList<T> items,
+            Function<? super T, ? extends C> cellFactory,
+            Gravity gravity) {
+        return new VirtualFlow<>(items, cellFactory, new HorizontalHelper(), gravity);
+    }
+
+    /**
+     * Creates a viewport that lays out content vertically from top to bottom
+     */
+    public static <T, C extends Cell<T, ?>> VirtualFlow<T, C> createVertical(
+            ObservableList<T> items,
+            Function<? super T, ? extends C> cellFactory) {
+        return createVertical(items, cellFactory, Gravity.FRONT);
+    }
+
+    /**
+     * Creates a viewport that lays out content vertically from top to bottom
+     */
+    public static <T, C extends Cell<T, ?>> VirtualFlow<T, C> createVertical(
+            ObservableList<T> items,
+            Function<? super T, ? extends C> cellFactory,
+            Gravity gravity) {
+        return new VirtualFlow<>(items, cellFactory, new VerticalHelper(), gravity);
+    }
+
+    private final ObservableList<T> items;
+    private final OrientationHelper orientation;
+    private final CellListManager<T, C> cellListManager;
+    private final SizeTracker sizeTracker;
+    private final CellPositioner<T, C> cellPositioner;
+    private final Navigator<T, C> navigator;
+
+    private final StyleableObjectProperty<Gravity> gravity = new StyleableObjectProperty<>() {
+        @Override
+        public Object getBean() {
+            return VirtualFlow.this;
+        }
+
+        @Override
+        public String getName() {
+            return "gravity";
+        }
+
+        @Override
+        public CssMetaData<? extends Styleable, Gravity> getCssMetaData() {
+            return GRAVITY;
+        }
+    };
+
+    // non-negative
+    private final Var<Double> breadthOffset0 = Var.newSimpleVar(0.0);
+    private final Var<Double> breadthOffset = breadthOffset0.asVar(this::setBreadthOffset);
+
+    public Var<Double> breadthOffsetProperty() {
+        return breadthOffset;
+    }
+
+    public Val<Double> totalBreadthEstimateProperty() {
+        return sizeTracker.maxCellBreadthProperty();
+    }
+
+    private final Var<Double> lengthOffsetEstimate;
+
+    public Var<Double> lengthOffsetEstimateProperty() {
+        return lengthOffsetEstimate;
+    }
+
+    private VirtualFlow(
+            ObservableList<T> items,
+            Function<? super T, ? extends C> cellFactory,
+            OrientationHelper orientation,
+            Gravity gravity) {
+        this.getStyleClass().add("virtual-flow");
+        this.items = items;
+        this.orientation = orientation;
+        this.cellListManager = new CellListManager<T, C>(this, items, cellFactory);
+        this.gravity.set(gravity);
+        MemoizationList<C> cells = cellListManager.getLazyCellList();
+        this.sizeTracker = new SizeTracker(orientation, layoutBoundsProperty(), cells);
+        this.cellPositioner = new CellPositioner<>(cellListManager, orientation, sizeTracker);
+        this.navigator = new Navigator<>(cellListManager, cellPositioner, orientation, this.gravity, sizeTracker);
+
+        getChildren().add(navigator);
+        clipProperty().bind(Val.map(
+                layoutBoundsProperty(),
+                b -> new Rectangle(b.getWidth(), b.getHeight())));
+
+        lengthOffsetEstimate = new StableBidirectionalVar<>(sizeTracker.lengthOffsetEstimateProperty(), this::setLengthOffset);
+
+        // scroll content by mouse scroll
+        this.addEventHandler(ScrollEvent.ANY, se -> {
+            scrollXBy(-se.getDeltaX());
+            scrollYBy(-se.getDeltaY());
+            se.consume();
+        });
+    }
+
+    public void dispose() {
+        navigator.dispose();
+        sizeTracker.dispose();
+        cellListManager.dispose();
+    }
+
+    /**
+     * If the item is out of view, instantiates a new cell for the item.
+     * The returned cell will be properly sized, but not properly positioned
+     * relative to the cells in the viewport, unless it is itself in the
+     * viewport.
+     *
+     * @return Cell for the given item. The cell will be valid only until the
+     * next layout pass. It should therefore not be stored. It is intended to
+     * be used for measurement purposes only.
+     */
+    public C getCell(int itemIndex) {
+        Lists.checkIndex(itemIndex, items.size());
+        return cellPositioner.getSizedCell(itemIndex);
+    }
+
+    /**
+     * This method calls {@link #layout()} as a side-effect to insure
+     * that the VirtualFlow is up-to-date in light of any changes
+     */
+    public Optional<C> getCellIfVisible(int itemIndex) {
+        // insure cells are up-to-date in light of any changes
+        layout();
+        return cellPositioner.getCellIfVisible(itemIndex);
+    }
+
+    /**
+     * This method calls {@link #layout()} as a side-effect to insure
+     * that the VirtualFlow is up-to-date in light of any changes
+     */
+    public ObservableList<C> visibleCells() {
+        // insure cells are up-to-date in light of any changes
+        layout();
+        return cellListManager.getLazyCellList().memoizedItems();
+    }
+
+    public Val<Double> totalLengthEstimateProperty() {
+        return sizeTracker.totalLengthEstimateProperty();
+    }
+
+    public Bounds cellToViewport(C cell, Bounds bounds) {
+        return cell.getNode().localToParent(bounds);
+    }
+
+    public Point2D cellToViewport(C cell, Point2D point) {
+        return cell.getNode().localToParent(point);
+    }
+
+    public Point2D cellToViewport(C cell, double x, double y) {
+        return cell.getNode().localToParent(x, y);
+    }
+
+    @Override
+    protected void layoutChildren() {
+
+        // navigate to the target position and fill viewport
+        while (true) {
+            double oldLayoutBreadth = sizeTracker.getCellLayoutBreadth();
+            orientation.resize(navigator, oldLayoutBreadth, sizeTracker.getViewportLength());
+            navigator.layout();
+            if (oldLayoutBreadth == sizeTracker.getCellLayoutBreadth()) {
+                break;
+            }
+        }
+
+        double viewBreadth = orientation.breadth(this);
+        double navigatorBreadth = orientation.breadth(navigator);
+        double totalBreadth = breadthOffset0.getValue();
+        double breadthDifference = navigatorBreadth - totalBreadth;
+        if (breadthDifference < viewBreadth) {
+            // viewport is scrolled all the way to the end of its breadth.
+            //  but now viewport size (breadth) has increased
+            double adjustment = viewBreadth - breadthDifference;
+            orientation.relocate(navigator, -(totalBreadth - adjustment), 0);
+            breadthOffset0.setValue(totalBreadth - adjustment);
+        } else {
+            orientation.relocate(navigator, -breadthOffset0.getValue(), 0);
+        }
+    }
+
+    @Override
+    protected final double computePrefWidth(double height) {
+        switch (getContentBias()) {
+            case HORIZONTAL: // vertical flow
+                return computePrefBreadth();
+            case VERTICAL: // horizontal flow
+                return computePrefLength(height);
+            default:
+                throw new AssertionError("Unreachable code");
+        }
+    }
+
+    @Override
+    protected final double computePrefHeight(double width) {
+        switch (getContentBias()) {
+            case HORIZONTAL: // vertical flow
+                return computePrefLength(width);
+            case VERTICAL: // horizontal flow
+                return computePrefBreadth();
+            default:
+                throw new AssertionError("Unreachable code");
+        }
+    }
+
+    private double computePrefBreadth() {
+        return 100;
+    }
+
+    private double computePrefLength(double breadth) {
+        return 100;
+    }
+
+    @Override
+    public final Orientation getContentBias() {
+        return orientation.getContentBias();
+    }
+
+    void scrollLength(double deltaLength) {
+        setLengthOffset(lengthOffsetEstimate.getValue() + deltaLength);
+    }
+
+    void scrollBreadth(double deltaBreadth) {
+        setBreadthOffset(breadthOffset0.getValue() + deltaBreadth);
+    }
+
+    /**
+     * Scroll the content horizontally by the given amount.
+     *
+     * @param deltaX positive value scrolls right, negative value scrolls left
+     */
+    @Override
+    public void scrollXBy(double deltaX) {
+        orientation.scrollHorizontallyBy(this, deltaX);
+    }
+
+    /**
+     * Scroll the content vertically by the given amount.
+     *
+     * @param deltaY positive value scrolls down, negative value scrolls up
+     */
+    @Override
+    public void scrollYBy(double deltaY) {
+        orientation.scrollVerticallyBy(this, deltaY);
+    }
+
+    /**
+     * Scroll the content horizontally to the pixel
+     *
+     * @param pixel - the pixel position to which to scroll
+     */
+    @Override
+    public void scrollXToPixel(double pixel) {
+        orientation.scrollHorizontallyToPixel(this, pixel);
+    }
+
+    /**
+     * Scroll the content vertically to the pixel
+     *
+     * @param pixel - the pixel position to which to scroll
+     */
+    @Override
+    public void scrollYToPixel(double pixel) {
+        orientation.scrollVerticallyToPixel(this, pixel);
+    }
+
+    @Override
+    public Val<Double> totalWidthEstimateProperty() {
+        return orientation.widthEstimateProperty(this);
+    }
+
+    @Override
+    public Val<Double> totalHeightEstimateProperty() {
+        return orientation.heightEstimateProperty(this);
+    }
+
+    @Override
+    public Var<Double> estimatedScrollXProperty() {
+        return orientation.estimatedScrollXProperty(this);
+    }
+
+    @Override
+    public Var<Double> estimatedScrollYProperty() {
+        return orientation.estimatedScrollYProperty(this);
+    }
+
+    /**
+     * Hits this virtual flow at the given coordinates.
+     *
+     * @param x x offset from the left edge of the viewport
+     * @param y y offset from the top edge of the viewport
+     * @return hit info containing the cell that was hit and coordinates
+     * relative to the cell. If the hit was before the cells (i.e. above a
+     * vertical flow content or left of a horizontal flow content), returns
+     * a <em>hit before cells</em> containing offset from the top left corner
+     * of the content. If the hit was after the cells (i.e. below a vertical
+     * flow content or right of a horizontal flow content), returns a
+     * <em>hit after cells</em> containing offset from the top right corner of
+     * the content of a horizontal flow or bottom left corner of the content of
+     * a vertical flow.
+     */
+    public VirtualFlowHit<C> hit(double x, double y) {
+        double bOff = orientation.getX(x, y);
+        double lOff = orientation.getY(x, y);
+
+        bOff += breadthOffset0.getValue();
+
+        if (items.isEmpty()) {
+            return orientation.hitAfterCells(bOff, lOff);
+        }
+
+        layout();
+
+        int firstVisible = getFirstVisibleIndex();
+        firstVisible = navigator.fillBackwardFrom0(firstVisible, lOff);
+        C firstCell = cellPositioner.getVisibleCell(firstVisible);
+
+        int lastVisible = getLastVisibleIndex();
+        lastVisible = navigator.fillForwardFrom0(lastVisible, lOff);
+        C lastCell = cellPositioner.getVisibleCell(lastVisible);
+
+        if (lOff < orientation.minY(firstCell)) {
+            return orientation.hitBeforeCells(bOff, lOff - orientation.minY(firstCell));
+        } else if (lOff >= orientation.maxY(lastCell)) {
+            return orientation.hitAfterCells(bOff, lOff - orientation.maxY(lastCell));
+        } else {
+            for (int i = firstVisible; i <= lastVisible; ++i) {
+                C cell = cellPositioner.getVisibleCell(i);
+                if (lOff < orientation.maxY(cell)) {
+                    return orientation.cellHit(i, cell, bOff, lOff - orientation.minY(cell));
+                }
+            }
+            throw new AssertionError("unreachable code");
+        }
+    }
+
+    /**
+     * Forces the viewport to acts as though it scrolled from 0 to {@code viewportOffset}). <em>Note:</em> the
+     * viewport makes an educated guess as to which cell is actually at {@code viewportOffset} if the viewport's
+     * entire content was completely rendered.
+     *
+     * @param viewportOffset See {@link OrientationHelper} and its implementations for explanation on what the offset
+     *                       means based on which implementation is used.
+     */
+    public void show(double viewportOffset) {
+        if (viewportOffset < 0) {
+            navigator.scrollCurrentPositionBy(viewportOffset);
+        } else if (viewportOffset > sizeTracker.getViewportLength()) {
+            navigator.scrollCurrentPositionBy(viewportOffset - sizeTracker.getViewportLength());
+        } else {
+            // do nothing, offset already in the viewport
+        }
+    }
+
+    /**
+     * Forces the viewport to show the given item by "scrolling" to it
+     */
+    public void show(int itemIdx) {
+        navigator.setTargetPosition(new MinDistanceTo(itemIdx));
+    }
+
+    /**
+     * Forces the viewport to show the given item as the first visible item as determined by its {@link Gravity}.
+     */
+    public void showAsFirst(int itemIdx) {
+        navigator.setTargetPosition(new StartOffStart(itemIdx, 0.0));
+    }
+
+    /**
+     * Forces the viewport to show the given item as the last visible item as determined by its {@link Gravity}.
+     */
+    public void showAsLast(int itemIdx) {
+        navigator.setTargetPosition(new EndOffEnd(itemIdx, 0.0));
+    }
+
+    /**
+     * Forces the viewport to show the given item by "scrolling" to it and then further "scrolling" by {@code offset}
+     * in one layout call (e.g., this method does not "scroll" twice)
+     *
+     * @param offset the offset value as determined by the viewport's {@link OrientationHelper}.
+     */
+    public void showAtOffset(int itemIdx, double offset) {
+        navigator.setTargetPosition(new StartOffStart(itemIdx, offset));
+    }
+
+    /**
+     * Forces the viewport to show the given item by "scrolling" to it and then further "scrolling," so that the
+     * {@code region} is visible, in one layout call (e.g., this method does not "scroll" twice).
+     */
+    public void show(int itemIndex, Bounds region) {
+        navigator.showLengthRegion(itemIndex, orientation.minY(region), orientation.maxY(region));
+        showBreadthRegion(orientation.minX(region), orientation.maxX(region));
+    }
+
+    /**
+     * Get the index of the first visible cell (at the time of the last layout).
+     *
+     * @return The index of the first visible cell
+     */
+    public int getFirstVisibleIndex() {
+        return navigator.getFirstVisibleIndex();
+    }
+
+    /**
+     * Get the index of the last visible cell (at the time of the last layout).
+     *
+     * @return The index of the last visible cell
+     */
+    public int getLastVisibleIndex() {
+        return navigator.getLastVisibleIndex();
+    }
+
+    private void showBreadthRegion(double fromX, double toX) {
+        double bOff = breadthOffset0.getValue();
+        double spaceBefore = fromX - bOff;
+        double spaceAfter = sizeTracker.getViewportBreadth() - toX + bOff;
+        if (spaceBefore < 0 && spaceAfter > 0) {
+            double shift = Math.min(-spaceBefore, spaceAfter);
+            setBreadthOffset(bOff - shift);
+        } else if (spaceAfter < 0 && spaceBefore > 0) {
+            double shift = Math.max(spaceAfter, -spaceBefore);
+            setBreadthOffset(bOff - shift);
+        }
+    }
+
+    void setLengthOffset(double pixels) {
+        double total = totalLengthEstimateProperty().getOrElse(0.0);
+        double length = sizeTracker.getViewportLength();
+        double max = Math.max(total - length, 0);
+        double current = lengthOffsetEstimate.getValue();
+
+        if (pixels > max) pixels = max;
+        if (pixels < 0) pixels = 0;
+
+        double diff = pixels - current;
+        if (diff == 0) {
+            // do nothing
+        } else if (Math.abs(diff) <= length) { // distance less than one screen
+            navigator.scrollCurrentPositionBy(diff);
+        } else {
+            jumpToAbsolutePosition(pixels);
+        }
+    }
+
+    void setBreadthOffset(double pixels) {
+        double total = totalBreadthEstimateProperty().getValue();
+        double breadth = sizeTracker.getViewportBreadth();
+        double max = Math.max(total - breadth, 0);
+        double current = breadthOffset0.getValue();
+
+        if (pixels > max) pixels = max;
+        if (pixels < 0) pixels = 0;
+
+        if (pixels != current) {
+            breadthOffset0.setValue(pixels);
+            requestLayout();
+            // TODO: could be safely relocated right away?
+            // (Does relocation request layout?)
+        }
+    }
+
+    private void jumpToAbsolutePosition(double pixels) {
+        if (items.isEmpty()) {
+            return;
+        }
+
+        // guess the first visible cell and its offset in the viewport
+        double avgLen = sizeTracker.getAverageLengthEstimate().orElse(0.0);
+        if (avgLen == 0.0) return;
+        int first = (int) Math.floor(pixels / avgLen);
+        double firstOffset = -(pixels % avgLen);
+
+        if (first < items.size()) {
+            navigator.setTargetPosition(new StartOffStart(first, firstOffset));
+        } else {
+            navigator.setTargetPosition(new EndOffEnd(items.size() - 1, 0.0));
+        }
+    }
+
+    /**
+     * The gravity of the virtual flow.  When there are not enough cells to fill
+     * the full height (vertical virtual flow) or width (horizontal virtual flow),
+     * the cells are placed either at the front (vertical: top, horizontal: left),
+     * or rear (vertical: bottom, horizontal: right) of the virtual flow, depending
+     * on the value of the gravity property.
+     * <p>
+     * The gravity can also be styled in CSS, using the "-flowless-gravity" property,
+     * for example:
+     * <pre>.virtual-flow { -flowless-gravity: rear; }</pre>
+     */
+    public ObjectProperty<Gravity> gravityProperty() {
+        return gravity;
+    }
+
+    public Gravity getGravity() {
+        return gravity.get();
+    }
+
+    public void setGravity(Gravity gravity) {
+        this.gravity.set(gravity);
+    }
+
+    @SuppressWarnings("unchecked") // Because of the cast we have to perform, below
+    private static final CssMetaData<VirtualFlow<?, ?>, Gravity> GRAVITY = new CssMetaData<>(
+            "-flowless-gravity",
+            // JavaFX seems to have an odd return type on getEnumConverter: "? extends Enum<?>", not E as the second generic type.
+            // Even though if you look at the source, the EnumConverter type it uses does have the type E.
+            // To get round this, we cast on return:
+            StyleConverter.getEnumConverter(Gravity.class),
+            Gravity.FRONT) {
+
+        @Override
+        public boolean isSettable(VirtualFlow virtualFlow) {
+            return !virtualFlow.gravity.isBound();
+        }
+
+        @Override
+        public StyleableProperty<Gravity> getStyleableProperty(VirtualFlow virtualFlow) {
+            return virtualFlow.gravity;
+        }
+    };
+
+    private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
+
+    static {
+        List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Region.getClassCssMetaData());
+        styleables.add(GRAVITY);
+        STYLEABLES = Collections.unmodifiableList(styleables);
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return STYLEABLES;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+        return getClassCssMetaData();
+    }
+}

+ 227 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/VirtualFlowHit.java

@@ -0,0 +1,227 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.geometry.Point2D;
+
+/**
+ * Stores the result of a {@link VirtualFlow#hit(double, double)}. Before calling
+ * any of the getters, one should determine what kind of hit this object is via {@link #isCellHit()},
+ * {@link #isBeforeCells()}, and {@link #isAfterCells()}. Otherwise, calling the wrong getter will throw
+ * an {@link UnsupportedOperationException}.
+ * <p>
+ * ypes of VirtualFlowHit:</p>
+ * <ul>
+ *     <li>
+ *         <em>Cell Hit:</em> a hit occurs on a displayed cell's node. One can call {@link #getCell()},
+ *         {@link #getCellIndex()}, and {@link #getCellOffset()}.
+ *     </li>
+ *     <li>
+ *         <em>Hit Before Cells:</em> a hit occurred before the displayed cells. One can call
+ *         {@link #getOffsetBeforeCells()}.
+ *     </li>
+ *     <li>
+ *         <em>Hit After Cells:</em> a hit occurred after the displayed cells. One can call
+ *         {@link #getOffsetAfterCells()}.
+ *     </li>
+ * </ul>
+ */
+public abstract class VirtualFlowHit<C extends Cell<?, ?>> {
+
+    static <C extends Cell<?, ?>> VirtualFlowHit<C> cellHit(
+            int cellIndex, C cell, double x, double y) {
+        return new CellHit<>(cellIndex, cell, new Point2D(x, y));
+    }
+
+    static <C extends Cell<?, ?>> VirtualFlowHit<C> hitBeforeCells(double x, double y) {
+        return new HitBeforeCells<>(new Point2D(x, y));
+    }
+
+    static <C extends Cell<?, ?>> VirtualFlowHit<C> hitAfterCells(double x, double y) {
+        return new HitAfterCells<>(new Point2D(x, y));
+    }
+
+    // private constructor to prevent subclassing
+    private VirtualFlowHit() {
+    }
+
+    public abstract boolean isCellHit();
+
+    public abstract boolean isBeforeCells();
+
+    public abstract boolean isAfterCells();
+
+    public abstract int getCellIndex();
+
+    public abstract C getCell();
+
+    public abstract Point2D getCellOffset();
+
+    public abstract Point2D getOffsetBeforeCells();
+
+    public abstract Point2D getOffsetAfterCells();
+
+    private static class CellHit<C extends Cell<?, ?>> extends VirtualFlowHit<C> {
+        private final int cellIdx;
+        private final C cell;
+        private final Point2D cellOffset;
+
+        CellHit(int cellIdx, C cell, Point2D cellOffset) {
+            this.cellIdx = cellIdx;
+            this.cell = cell;
+            this.cellOffset = cellOffset;
+        }
+
+        @Override
+        public boolean isCellHit() {
+            return true;
+        }
+
+        @Override
+        public boolean isBeforeCells() {
+            return false;
+        }
+
+        @Override
+        public boolean isAfterCells() {
+            return false;
+        }
+
+        @Override
+        public int getCellIndex() {
+            return cellIdx;
+        }
+
+        @Override
+        public C getCell() {
+            return cell;
+        }
+
+        @Override
+        public Point2D getCellOffset() {
+            return cellOffset;
+        }
+
+        @Override
+        public Point2D getOffsetBeforeCells() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Point2D getOffsetAfterCells() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static class HitBeforeCells<C extends Cell<?, ?>> extends VirtualFlowHit<C> {
+        private final Point2D offset;
+
+        HitBeforeCells(Point2D offset) {
+            this.offset = offset;
+        }
+
+        @Override
+        public boolean isCellHit() {
+            return false;
+        }
+
+        @Override
+        public boolean isBeforeCells() {
+            return true;
+        }
+
+        @Override
+        public boolean isAfterCells() {
+            return false;
+        }
+
+        @Override
+        public int getCellIndex() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public C getCell() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Point2D getCellOffset() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Point2D getOffsetBeforeCells() {
+            return offset;
+        }
+
+        @Override
+        public Point2D getOffsetAfterCells() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static class HitAfterCells<C extends Cell<?, ?>> extends VirtualFlowHit<C> {
+        private final Point2D offset;
+
+        HitAfterCells(Point2D offset) {
+            this.offset = offset;
+        }
+
+        @Override
+        public boolean isCellHit() {
+            return false;
+        }
+
+        @Override
+        public boolean isBeforeCells() {
+            return false;
+        }
+
+        @Override
+        public boolean isAfterCells() {
+            return true;
+        }
+
+        @Override
+        public int getCellIndex() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public C getCell() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Point2D getCellOffset() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Point2D getOffsetBeforeCells() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Point2D getOffsetAfterCells() {
+            return offset;
+        }
+    }
+}

+ 119 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/Virtualized.java

@@ -0,0 +1,119 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.geometry.Point2D;
+import org.reactfx.value.Val;
+import org.reactfx.value.Var;
+
+/**
+ * Specifies an object that does not have scroll bars by default but which can have scroll bars added to it
+ * by wrapping it in a {@link VirtualizedScrollPane}.
+ */
+public interface Virtualized {
+
+    Val<Double> totalWidthEstimateProperty();
+
+    default double getTotalWidthEstimate() {
+        return totalHeightEstimateProperty().getValue();
+    }
+
+    Val<Double> totalHeightEstimateProperty();
+
+    default double getTotalHeightEstimate() {
+        return totalHeightEstimateProperty().getValue();
+    }
+
+    Var<Double> estimatedScrollXProperty();
+
+    default double getEstimatedScrollX() {
+        return estimatedScrollXProperty().getValue();
+    }
+
+    Var<Double> estimatedScrollYProperty();
+
+    default double getEstimatedScrollY() {
+        return estimatedScrollYProperty().getValue();
+    }
+
+    /**
+     * Convenience method: scroll horizontally by {@code deltas.getX()} and vertically by {@code deltas.getY()}
+     *
+     * @param deltas negative values scroll left/up, positive scroll right/down
+     */
+    default void scrollBy(Point2D deltas) {
+        scrollXBy(deltas.getX());
+        scrollYBy(deltas.getY());
+    }
+
+    /**
+     * Convenience method: scroll horizontally by {@code deltaX} and vertically by {@code deltaY}
+     *
+     * @param deltaX negative values scroll left, positive scroll right
+     * @param deltaY negative values scroll up, positive scroll down
+     */
+    default void scrollBy(double deltaX, double deltaY) {
+        scrollXBy(deltaX);
+        scrollYBy(deltaY);
+    }
+
+    /**
+     * Scroll the content horizontally by the given amount.
+     *
+     * @param deltaX positive value scrolls right, negative value scrolls left
+     */
+    void scrollXBy(double deltaX);
+
+    /**
+     * Scroll the content vertically by the given amount.
+     *
+     * @param deltaY positive value scrolls down, negative value scrolls up
+     */
+    void scrollYBy(double deltaY);
+
+    /**
+     * Convenicen method: scroll the content to the pixel
+     */
+    default void scrollToPixel(Point2D pixel) {
+        scrollXToPixel(pixel.getX());
+        scrollYToPixel(pixel.getY());
+    }
+
+    /**
+     * Convenicen method: scroll the content to the pixel
+     */
+    default void scrollToPixel(double xPixel, double yPixel) {
+        scrollXToPixel(xPixel);
+        scrollYToPixel(yPixel);
+    }
+
+    /**
+     * Scroll the content horizontally to the pixel
+     *
+     * @param pixel - the pixel position to which to scroll
+     */
+    void scrollXToPixel(double pixel);
+
+    /**
+     * Scroll the content vertically to the pixel
+     *
+     * @param pixel - the pixel position to which to scroll
+     */
+    void scrollYToPixel(double pixel);
+}

+ 383 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/flowless/VirtualizedScrollPane.java

@@ -0,0 +1,383 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.flowless;
+
+import javafx.application.Platform;
+import javafx.beans.DefaultProperty;
+import javafx.beans.NamedArg;
+import javafx.beans.Observable;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.DoubleBinding;
+import javafx.beans.value.ChangeListener;
+import javafx.css.PseudoClass;
+import javafx.geometry.Bounds;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.Region;
+import org.reactfx.value.Val;
+import org.reactfx.value.Var;
+
+import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
+
+@DefaultProperty("content")
+public class VirtualizedScrollPane<V extends Node & Virtualized> extends Region implements Virtualized {
+
+    private static final PseudoClass CONTENT_FOCUSED = PseudoClass.getPseudoClass("content-focused");
+
+    private final ScrollBar hBar;
+    private final ScrollBar vBar;
+    private final V content;
+    private final ChangeListener<Boolean> contentFocusedListener;
+
+    private final Var<Double> hBarValue;
+    private final Var<Double> vBarValue;
+
+    /**
+     * The Policy for the Horizontal ScrollBar
+     */
+    private final Var<ScrollPane.ScrollBarPolicy> hBarPolicy;
+
+    public final ScrollPane.ScrollBarPolicy getHBarPolicy() {
+        return hBarPolicy.getValue();
+    }
+
+    public final void setHBarPolicy(ScrollPane.ScrollBarPolicy value) {
+        hBarPolicy.setValue(value);
+    }
+
+    public final Var<ScrollPane.ScrollBarPolicy> hBarPolicyProperty() {
+        return hBarPolicy;
+    }
+
+    /**
+     * The Policy for the Vertical ScrollBar
+     */
+    private final Var<ScrollPane.ScrollBarPolicy> vBarPolicy;
+
+    public final ScrollPane.ScrollBarPolicy getVBarPolicy() {
+        return vBarPolicy.getValue();
+    }
+
+    public final void setVBarPolicy(ScrollPane.ScrollBarPolicy value) {
+        vBarPolicy.setValue(value);
+    }
+
+    public final Var<ScrollPane.ScrollBarPolicy> vBarPolicyProperty() {
+        return vBarPolicy;
+    }
+
+    /**
+     * Constructs a VirtualizedScrollPane with the given content and policies
+     */
+    public VirtualizedScrollPane(
+            @NamedArg("content") V content,
+            @NamedArg("hPolicy") ScrollPane.ScrollBarPolicy hPolicy,
+            @NamedArg("vPolicy") ScrollPane.ScrollBarPolicy vPolicy
+    ) {
+        this.getStyleClass().add("virtualized-scroll-pane");
+        this.content = content;
+
+        // create scrollbars
+        hBar = new ScrollBar();
+        vBar = new ScrollBar();
+        hBar.setOrientation(Orientation.HORIZONTAL);
+        vBar.setOrientation(Orientation.VERTICAL);
+
+        // scrollbar ranges
+        hBar.setMin(0);
+        vBar.setMin(0);
+        hBar.maxProperty().bind(content.totalWidthEstimateProperty());
+        vBar.maxProperty().bind(content.totalHeightEstimateProperty());
+
+        // scrollbar increments
+        setupUnitIncrement(hBar);
+        setupUnitIncrement(vBar);
+        hBar.blockIncrementProperty().bind(hBar.visibleAmountProperty());
+        vBar.blockIncrementProperty().bind(vBar.visibleAmountProperty());
+
+        // scrollbar positions
+        Var<Double> hPosEstimate = Val
+                .combine(
+                        content.estimatedScrollXProperty(),
+                        Val.map(content.layoutBoundsProperty(), Bounds::getWidth),
+                        content.totalWidthEstimateProperty(),
+                        VirtualizedScrollPane::offsetToScrollbarPosition)
+                .asVar(this::setHPosition);
+        Var<Double> vPosEstimate = Val
+                .combine(
+                        content.estimatedScrollYProperty(),
+                        Val.map(content.layoutBoundsProperty(), Bounds::getHeight),
+                        content.totalHeightEstimateProperty(),
+                        VirtualizedScrollPane::offsetToScrollbarPosition)
+                .orElseConst(0.0)
+                .asVar(this::setVPosition);
+        hBarValue = Var.doubleVar(hBar.valueProperty());
+        vBarValue = Var.doubleVar(vBar.valueProperty());
+        Bindings.bindBidirectional(hBarValue, hPosEstimate);
+        Bindings.bindBidirectional(vBarValue, vPosEstimate);
+
+        // scrollbar visibility
+        hBarPolicy = Var.newSimpleVar(hPolicy);
+        vBarPolicy = Var.newSimpleVar(vPolicy);
+
+        Val<Double> layoutWidth = Val.map(layoutBoundsProperty(), Bounds::getWidth);
+        Val<Double> layoutHeight = Val.map(layoutBoundsProperty(), Bounds::getHeight);
+        Val<Boolean> needsHBar0 = Val.combine(
+                content.totalWidthEstimateProperty(),
+                layoutWidth,
+                (cw, lw) -> cw > lw);
+        Val<Boolean> needsVBar0 = Val.combine(
+                content.totalHeightEstimateProperty(),
+                layoutHeight,
+                (ch, lh) -> ch > lh);
+        Val<Boolean> needsHBar = Val.combine(
+                needsHBar0,
+                needsVBar0,
+                content.totalWidthEstimateProperty(),
+                vBar.widthProperty(),
+                layoutWidth,
+                (needsH, needsV, cw, vbw, lw) -> needsH || needsV && cw + vbw.doubleValue() > lw);
+        Val<Boolean> needsVBar = Val.combine(
+                needsVBar0,
+                needsHBar0,
+                content.totalHeightEstimateProperty(),
+                hBar.heightProperty(),
+                layoutHeight,
+                (needsV, needsH, ch, hbh, lh) -> needsV || needsH && ch + hbh.doubleValue() > lh);
+
+        Val<Boolean> shouldDisplayHorizontal = Val.flatMap(hBarPolicy, policy -> {
+            switch (policy) {
+                case NEVER:
+                    return Val.constant(false);
+                case ALWAYS:
+                    return Val.constant(true);
+                default: // AS_NEEDED
+                    return needsHBar;
+            }
+        });
+        Val<Boolean> shouldDisplayVertical = Val.flatMap(vBarPolicy, policy -> {
+            switch (policy) {
+                case NEVER:
+                    return Val.constant(false);
+                case ALWAYS:
+                    return Val.constant(true);
+                default: // AS_NEEDED
+                    return needsVBar;
+            }
+        });
+
+        // request layout later, because if currently in layout, the request is ignored
+        shouldDisplayHorizontal.addListener(obs -> Platform.runLater(this::requestLayout));
+        shouldDisplayVertical.addListener(obs -> Platform.runLater(this::requestLayout));
+
+        hBar.visibleProperty().bind(shouldDisplayHorizontal);
+        vBar.visibleProperty().bind(shouldDisplayVertical);
+
+        contentFocusedListener = (obs, ov, nv) -> pseudoClassStateChanged(CONTENT_FOCUSED, nv);
+        content.focusedProperty().addListener(contentFocusedListener);
+        getChildren().addAll(content, hBar, vBar);
+        getChildren().addListener((Observable obs) -> dispose());
+    }
+
+    /**
+     * Constructs a VirtualizedScrollPane that only displays its horizontal and vertical scroll bars as needed
+     */
+    public VirtualizedScrollPane(@NamedArg("content") V content) {
+        this(content, AS_NEEDED, AS_NEEDED);
+    }
+
+    /**
+     * Does not unbind scrolling from Content before returning Content.
+     *
+     * @return - the content
+     */
+    public V getContent() {
+        return content;
+    }
+
+    /**
+     * Unbinds scrolling from Content before returning Content.
+     *
+     * @return - the content
+     */
+    public V removeContent() {
+        getChildren().clear();
+        return content;
+    }
+
+    private void dispose() {
+        content.focusedProperty().removeListener(contentFocusedListener);
+        hBarValue.unbindBidirectional(content.estimatedScrollXProperty());
+        vBarValue.unbindBidirectional(content.estimatedScrollYProperty());
+        unbindScrollBar(hBar);
+        unbindScrollBar(vBar);
+    }
+
+    private void unbindScrollBar(ScrollBar bar) {
+        bar.maxProperty().unbind();
+        bar.unitIncrementProperty().unbind();
+        bar.blockIncrementProperty().unbind();
+        bar.visibleProperty().unbind();
+    }
+
+    @Override
+    public Val<Double> totalWidthEstimateProperty() {
+        return content.totalWidthEstimateProperty();
+    }
+
+    @Override
+    public Val<Double> totalHeightEstimateProperty() {
+        return content.totalHeightEstimateProperty();
+    }
+
+    @Override
+    public Var<Double> estimatedScrollXProperty() {
+        return content.estimatedScrollXProperty();
+    }
+
+    @Override
+    public Var<Double> estimatedScrollYProperty() {
+        return content.estimatedScrollYProperty();
+    }
+
+    @Override
+    public void scrollXBy(double deltaX) {
+        content.scrollXBy(deltaX);
+    }
+
+    @Override
+    public void scrollYBy(double deltaY) {
+        content.scrollYBy(deltaY);
+    }
+
+    @Override
+    public void scrollXToPixel(double pixel) {
+        content.scrollXToPixel(pixel);
+    }
+
+    @Override
+    public void scrollYToPixel(double pixel) {
+        content.scrollYToPixel(pixel);
+    }
+
+    @Override
+    protected double computePrefWidth(double height) {
+        return content.prefWidth(height);
+    }
+
+    @Override
+    protected double computePrefHeight(double width) {
+        return content.prefHeight(width);
+    }
+
+    @Override
+    protected double computeMinWidth(double height) {
+        return vBar.minWidth(-1);
+    }
+
+    @Override
+    protected double computeMinHeight(double width) {
+        return hBar.minHeight(-1);
+    }
+
+    @Override
+    protected double computeMaxWidth(double height) {
+        return content.maxWidth(height);
+    }
+
+    @Override
+    protected double computeMaxHeight(double width) {
+        return content.maxHeight(width);
+    }
+
+    @Override
+    protected void layoutChildren() {
+        double layoutWidth = snapSizeX(getLayoutBounds().getWidth());
+        double layoutHeight = snapSizeY(getLayoutBounds().getHeight());
+        boolean vbarVisible = vBar.isVisible();
+        boolean hbarVisible = hBar.isVisible();
+        double vbarWidth = snapSizeX(vbarVisible ? vBar.prefWidth(-1) : 0);
+        double hbarHeight = snapSizeY(hbarVisible ? hBar.prefHeight(-1) : 0);
+
+        double w = layoutWidth - vbarWidth;
+        double h = layoutHeight - hbarHeight;
+
+        content.resize(w, h);
+
+        hBar.setVisibleAmount(w);
+        vBar.setVisibleAmount(h);
+
+        if (vbarVisible) {
+            vBar.resizeRelocate(layoutWidth - vbarWidth, 0, vbarWidth, h);
+        }
+
+        if (hbarVisible) {
+            hBar.resizeRelocate(0, layoutHeight - hbarHeight, w, hbarHeight);
+        }
+    }
+
+    private void setHPosition(double pos) {
+        double offset = scrollbarPositionToOffset(
+                pos,
+                content.getLayoutBounds().getWidth(),
+                content.totalWidthEstimateProperty().getValue());
+        content.estimatedScrollXProperty().setValue(offset);
+    }
+
+    private void setVPosition(double pos) {
+        double offset = scrollbarPositionToOffset(
+                pos,
+                content.getLayoutBounds().getHeight(),
+                content.totalHeightEstimateProperty().getValue());
+        content.estimatedScrollYProperty().setValue(offset);
+    }
+
+    private static void setupUnitIncrement(ScrollBar bar) {
+        bar.unitIncrementProperty().bind(new DoubleBinding() {
+            {
+                bind(bar.maxProperty(), bar.visibleAmountProperty());
+            }
+
+            @Override
+            protected double computeValue() {
+                double max = bar.getMax();
+                double visible = bar.getVisibleAmount();
+                return max > visible
+                        ? 16 / (max - visible) * max
+                        : 0;
+            }
+        });
+    }
+
+    private static double offsetToScrollbarPosition(
+            double contentOffset, double viewportSize, double contentSize) {
+        return contentSize > viewportSize
+                ? contentOffset / (contentSize - viewportSize) * contentSize
+                : 0;
+    }
+
+    private static double scrollbarPositionToOffset(
+            double scrollbarPos, double viewportSize, double contentSize) {
+        return contentSize > viewportSize
+                ? scrollbarPos / contentSize * (contentSize - viewportSize)
+                : 0;
+    }
+}

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java

@@ -20,7 +20,7 @@ package io.github.palexdev.materialfx.controls.legacy;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
-import io.github.palexdev.materialfx.controls.cell.legacy.MFXLegacyListCell;
+import io.github.palexdev.materialfx.controls.cell.MFXListCell;
 import io.github.palexdev.materialfx.skins.legacy.MFXLegacyComboBoxSkin;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
 import javafx.beans.property.BooleanProperty;
@@ -88,7 +88,7 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> {
     //================================================================================
     private void initialize() {
         getStyleClass().add(STYLE_CLASS);
-        setCellFactory(listCell -> new MFXLegacyListCell<>() {
+        setCellFactory(listCell -> new MFXListCell<>() {
             @Override
             protected void updateItem(T item, boolean empty) {
                 super.updateItem(item, empty);

+ 0 - 17
materialfx/src/main/java/io/github/palexdev/materialfx/selection/ITableSelectionModel.java

@@ -1,17 +0,0 @@
-package io.github.palexdev.materialfx.selection;
-
-import io.github.palexdev.materialfx.controls.MFXTableRow;
-import javafx.beans.property.ListProperty;
-import javafx.scene.input.MouseEvent;
-
-/**
- * Public API used by any {@code MFXTableView}.
- */
-public interface ITableSelectionModel<T> {
-    void select(MFXTableRow<T> row, MouseEvent mouseEvent);
-    void clearSelection();
-    MFXTableRow<T> getSelectedRow();
-    ListProperty<MFXTableRow<T>> getSelectedRows();
-    boolean allowsMultipleSelection();
-    void setAllowsMultipleSelection(boolean multipleSelection);
-}

+ 116 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/selection/ListSelectionModel.java

@@ -0,0 +1,116 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.selection;
+
+import io.github.palexdev.materialfx.controls.base.AbstractMFXFlowlessListCell;
+import io.github.palexdev.materialfx.selection.base.IListSelectionModel;
+import javafx.beans.property.ListProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.scene.input.MouseEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ListSelectionModel<T> implements IListSelectionModel<T> {
+    private final ListProperty<AbstractMFXFlowlessListCell<T>> selectedItems = new SimpleListProperty<>(FXCollections.observableArrayList());
+    private boolean allowsMultipleSelection = false;
+
+    public ListSelectionModel() {
+        selectedItems.addListener((ListChangeListener<AbstractMFXFlowlessListCell<T>>) change -> {
+            List<AbstractMFXFlowlessListCell<T>> tmpRemoved = new ArrayList<>();
+            List<AbstractMFXFlowlessListCell<T>> tmpAdded = new ArrayList<>();
+
+            while (change.next()) {
+                tmpRemoved.addAll(change.getRemoved());
+                tmpAdded.addAll(change.getAddedSubList());
+            }
+            tmpRemoved.forEach(item -> item.setSelected(false));
+            tmpAdded.forEach(item -> item.setSelected(true));
+        });
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void select(AbstractMFXFlowlessListCell<T> item) {
+        if (!allowsMultipleSelection) {
+            clearSelection();
+            selectedItems.setAll(item);
+        } else {
+            selectedItems.add(item);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void select(AbstractMFXFlowlessListCell<T> item, MouseEvent mouseEvent) {
+        if (mouseEvent == null) {
+            select(item);
+            return;
+        }
+
+        if (!allowsMultipleSelection && !selectedItems.contains(item)) {
+            selectedItems.setAll(item);
+            return;
+        }
+
+        if (mouseEvent.isShiftDown() || mouseEvent.isControlDown()) {
+            if (item.isSelected()) {
+                selectedItems.remove(item);
+            } else {
+                selectedItems.add(item);
+            }
+        } else if (!selectedItems.contains(item)) {
+            selectedItems.setAll(item);
+        }
+    }
+
+    @Override
+    public void clearSelection() {
+        if (selectedItems.isEmpty()) {
+            return;
+        }
+
+        selectedItems.forEach(item -> item.setSelected(false));
+        selectedItems.clear();
+    }
+
+    @Override
+    public AbstractMFXFlowlessListCell<T> getSelectedItem() {
+        if (selectedItems.isEmpty()) {
+            return null;
+        }
+        return selectedItems.get(0);
+    }
+
+    @Override
+    public ListProperty<AbstractMFXFlowlessListCell<T>> getSelectedItems() {
+        return this.selectedItems;
+    }
+
+    @Override
+    public boolean allowsMultipleSelection() {
+        return allowsMultipleSelection;
+    }
+
+    @Override
+    public void setAllowsMultipleSelection(boolean multipleSelection) {
+        this.allowsMultipleSelection = multipleSelection;
+    }
+}

+ 1 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/selection/TableSelectionModel.java

@@ -1,6 +1,7 @@
 package io.github.palexdev.materialfx.selection;
 
 import io.github.palexdev.materialfx.controls.MFXTableRow;
+import io.github.palexdev.materialfx.selection.base.ITableSelectionModel;
 import javafx.beans.property.ListProperty;
 import javafx.beans.property.SimpleListProperty;
 import javafx.collections.FXCollections;

+ 1 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/selection/TreeCheckModel.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.selection;
 
 import io.github.palexdev.materialfx.controls.MFXCheckTreeItem;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
+import io.github.palexdev.materialfx.selection.base.ITreeCheckModel;
 import io.github.palexdev.materialfx.utils.TreeItemStream;
 import javafx.beans.property.ListProperty;
 import javafx.beans.property.SimpleListProperty;

+ 1 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/selection/TreeSelectionModel.java

@@ -20,6 +20,7 @@ package io.github.palexdev.materialfx.selection;
 
 import io.github.palexdev.materialfx.controls.MFXTreeItem;
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
+import io.github.palexdev.materialfx.selection.base.ITreeSelectionModel;
 import io.github.palexdev.materialfx.utils.TreeItemStream;
 import javafx.beans.property.ListProperty;
 import javafx.beans.property.SimpleListProperty;

+ 32 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/IListSelectionModel.java

@@ -0,0 +1,32 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.selection.base;
+
+import io.github.palexdev.materialfx.controls.base.AbstractMFXFlowlessListCell;
+import javafx.beans.property.ListProperty;
+import javafx.scene.input.MouseEvent;
+
+public interface IListSelectionModel<T> {
+    void select(AbstractMFXFlowlessListCell<T> item, MouseEvent mouseEvent);
+    void clearSelection();
+    AbstractMFXFlowlessListCell<T> getSelectedItem();
+    ListProperty<AbstractMFXFlowlessListCell<T>> getSelectedItems();
+    boolean allowsMultipleSelection();
+    void setAllowsMultipleSelection(boolean multipleSelection);
+}

+ 35 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITableSelectionModel.java

@@ -0,0 +1,35 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.selection.base;
+
+import io.github.palexdev.materialfx.controls.MFXTableRow;
+import javafx.beans.property.ListProperty;
+import javafx.scene.input.MouseEvent;
+
+/**
+ * Public API used by any {@code MFXTableView}.
+ */
+public interface ITableSelectionModel<T> {
+    void select(MFXTableRow<T> row, MouseEvent mouseEvent);
+    void clearSelection();
+    MFXTableRow<T> getSelectedRow();
+    ListProperty<MFXTableRow<T>> getSelectedRows();
+    boolean allowsMultipleSelection();
+    void setAllowsMultipleSelection(boolean multipleSelection);
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/selection/ITreeCheckModel.java → materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITreeCheckModel.java

@@ -16,7 +16,7 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.selection;
+package io.github.palexdev.materialfx.selection.base;
 
 import io.github.palexdev.materialfx.controls.MFXCheckTreeItem;
 import javafx.beans.property.ListProperty;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/selection/ITreeSelectionModel.java → materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITreeSelectionModel.java

@@ -16,7 +16,7 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.selection;
+package io.github.palexdev.materialfx.selection.base;
 
 import io.github.palexdev.materialfx.controls.base.AbstractMFXTreeItem;
 import javafx.beans.property.ListProperty;

+ 3 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java

@@ -21,9 +21,9 @@ package io.github.palexdev.materialfx.skins;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
 import io.github.palexdev.materialfx.controls.MFXComboBox;
 import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXListView;
 import io.github.palexdev.materialfx.controls.enums.Styles;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.legacy.MFXLegacyListView;
 import io.github.palexdev.materialfx.effects.RippleGenerator;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.selection.ComboSelectionModelMock;
@@ -59,7 +59,7 @@ public class MFXComboBoxSkin<T> extends SkinBase<MFXComboBox<T>> {
 
     private final MFXIconWrapper icon;
     private final PopupControl popup;
-    private final MFXLegacyListView<T> listView;
+    private final MFXListView<T> listView;
     private final EventHandler<MouseEvent> popupHandler;
 
     private final Line unfocusedLine;
@@ -101,7 +101,7 @@ public class MFXComboBoxSkin<T> extends SkinBase<MFXComboBox<T>> {
         container = new HBox(20, valueLabel);
         container.setAlignment(Pos.CENTER_LEFT);
 
-        listView = new MFXLegacyListView<>();
+        listView = new MFXListView<>();
         listView.getStylesheets().add(comboBox.getUserAgentStylesheet());
         popup = buildPopup();
 

+ 3 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java

@@ -3,10 +3,10 @@ package io.github.palexdev.materialfx.skins;
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
 import io.github.palexdev.materialfx.controls.MFXFilterComboBox;
 import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXListView;
 import io.github.palexdev.materialfx.controls.MFXTextField;
 import io.github.palexdev.materialfx.controls.enums.Styles;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.legacy.MFXLegacyListView;
 import io.github.palexdev.materialfx.effects.RippleGenerator;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.selection.ComboSelectionModelMock;
@@ -46,7 +46,7 @@ public class MFXFilterComboBoxSkin<T> extends SkinBase<MFXFilterComboBox<T>> {
 
     private final MFXIconWrapper icon;
     private final PopupControl popup;
-    private final MFXLegacyListView<T> listView;
+    private final MFXListView<T> listView;
     private final EventHandler<MouseEvent> popupHandler;
 
     private final Line unfocusedLine;
@@ -103,7 +103,7 @@ public class MFXFilterComboBoxSkin<T> extends SkinBase<MFXFilterComboBox<T>> {
         searchContainer.setAlignment(Pos.CENTER_LEFT);
         searchContainer.setManaged(false);
 
-        listView = new MFXLegacyListView<>();
+        listView = new MFXListView<>();
         listView.getStylesheets().add(comboBox.getUserAgentStylesheet());
         popup = buildPopup();
         popupHandler = event -> {

+ 170 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFlowlessListViewSkin.java

@@ -0,0 +1,170 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXFlowlessListView;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXFlowlessListCell;
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.controls.flowless.MFXVirtualizedScrollPane;
+import io.github.palexdev.materialfx.controls.flowless.VirtualFlow;
+import io.github.palexdev.materialfx.effects.MFXDepthManager;
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.util.Duration;
+
+public class MFXFlowlessListViewSkin<T> extends SkinBase<MFXFlowlessListView<T>> {
+    private final ScrollBar vBar;
+    private Timeline hideBars;
+    private Timeline showBars;
+
+    public MFXFlowlessListViewSkin(MFXFlowlessListView<T> listView) {
+        super(listView);
+
+        VirtualFlow<T, AbstractMFXFlowlessListCell<T>> flow = VirtualFlow.createVertical(listView.getItems(), this::createCell);
+        flow.setId("virtualFlow");
+        flow.getStylesheets().setAll(listView.getUserAgentStylesheet());
+
+        MFXVirtualizedScrollPane<VirtualFlow<T, AbstractMFXFlowlessListCell<T>>> virtualScrollPane = new MFXVirtualizedScrollPane<>(flow);
+        virtualScrollPane.setId("virtualScrollPane");
+        virtualScrollPane.getStylesheets().setAll(listView.getUserAgentStylesheet());
+        //MFXVirtualizedScrollPane.smoothVScrolling(virtualScrollPane); // Not working atm
+
+        vBar = (ScrollBar) virtualScrollPane.lookup(".vvbar");
+
+        if (vBar != null) {
+            hideBars = new Timeline(
+                    new KeyFrame(Duration.millis(400),
+                            new KeyValue(vBar.opacityProperty(), 0.0, MFXAnimationFactory.getInterpolatorV2())));
+            showBars = new Timeline(
+                    new KeyFrame(Duration.millis(400),
+                            new KeyValue(vBar.opacityProperty(), 1.0, MFXAnimationFactory.getInterpolatorV2())));
+        }
+
+        if (vBar != null && listView.isHideScrollBars()) {
+            vBar.setOpacity(0.0);
+        }
+
+        getChildren().add(virtualScrollPane);
+
+        listView.setEffect(MFXDepthManager.shadowOf(listView.getDepthLevel()));
+        setListeners();
+    }
+
+    private void setListeners() {
+        MFXFlowlessListView<T> listView = getSkinnable();
+
+        setScrollBarHandlers();
+        listView.depthLevelProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                listView.setEffect(MFXDepthManager.shadowOf(listView.getDepthLevel()));
+            }
+        });
+    }
+
+    private void setScrollBarHandlers() {
+        if (vBar != null) {
+            MFXFlowlessListView<T> listView = getSkinnable();
+
+            listView.setOnMouseExited(event -> {
+                if (listView.isHideScrollBars()) {
+                    hideBars.setDelay(listView.getHideAfter());
+
+                    if (vBar.isPressed()) {
+                        vBar.pressedProperty().addListener(new ChangeListener<>() {
+                            @Override
+                            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+                                if (!newValue) {
+                                    hideBars.play();
+                                }
+                                vBar.pressedProperty().removeListener(this);
+                            }
+                        });
+                        return;
+                    }
+
+                    hideBars.play();
+                }
+            });
+
+            listView.setOnMouseEntered(event -> {
+                if (hideBars.getStatus().equals(Animation.Status.RUNNING)) {
+                    hideBars.stop();
+                }
+                showBars.play();
+            });
+
+            listView.hideScrollBarsProperty().addListener((observable, oldValue, newValue) -> {
+                if (newValue) {
+                    hideBars.play();
+                } else {
+                    showBars.play();
+                }
+                if (newValue &&
+                        hideBars.getStatus() != Animation.Status.RUNNING ||
+                        vBar.getOpacity() != 0
+                ) {
+                    vBar.setOpacity(0.0);
+                }
+            });
+        }
+    }
+
+    protected AbstractMFXFlowlessListCell<T> createCell(T item) {
+        MFXFlowlessListView<T> listView = getSkinnable();
+
+        AbstractMFXFlowlessListCell<T> cell = listView.getCellFactory().call(item);
+        setCellBehavior(cell);
+        return cell;
+    }
+
+    protected void setCellBehavior(AbstractMFXFlowlessListCell<T> cell) {
+        MFXFlowlessListView<T> listView = getSkinnable();
+
+        cell.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> listView.getSelectionModel().select(cell, event));
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return leftInset + 200 + rightInset;
+    }
+
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return topInset + 350 + bottomInset;
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+
+        if (hideBars != null) {
+            hideBars = null;
+        }
+        if (showBars != null) {
+            showBars = null;
+        }
+    }
+}

+ 7 - 7
materialfx/src/main/java/io/github/palexdev/materialfx/skins/legacy/MFXLegacyListViewSkin.java → materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXListViewSkin.java

@@ -16,10 +16,10 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.github.palexdev.materialfx.skins.legacy;
+package io.github.palexdev.materialfx.skins;
 
+import io.github.palexdev.materialfx.controls.MFXListView;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
-import io.github.palexdev.materialfx.controls.legacy.MFXLegacyListView;
 import io.github.palexdev.materialfx.effects.MFXDepthManager;
 import javafx.animation.Animation;
 import javafx.animation.KeyFrame;
@@ -40,12 +40,12 @@ import javafx.util.Duration;
 import java.util.Set;
 
 /**
- * This is the implementation of the {@code Skin} associated with every {@code MFXLegacyListView}.
+ * This is the implementation of the {@code Skin} associated with every {@code MFXListView}.
  * <p>
  * The most important thing this skin does is replacing the default scrollbars with new ones,
  * this makes styling them a lot more easy.
  */
-public class MFXLegacyListViewSkin<T> extends ListViewSkin<T> {
+public class MFXListViewSkin<T> extends ListViewSkin<T> {
     //================================================================================
     // Properties
     //================================================================================
@@ -60,7 +60,7 @@ public class MFXLegacyListViewSkin<T> extends ListViewSkin<T> {
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXLegacyListViewSkin(final MFXLegacyListView<T> listView) {
+    public MFXListViewSkin(final MFXListView<T> listView) {
         super(listView);
 
         virtualFlow = (VirtualFlow<?>) listView.lookup(".virtual-flow");
@@ -108,7 +108,7 @@ public class MFXLegacyListViewSkin<T> extends ListViewSkin<T> {
      * Adds listeners for: mouseExited, mouseEntered, hideScrollBars, and depthLevel properties.
      */
     private void setListeners() {
-        MFXLegacyListView<T> listView = (MFXLegacyListView<T>) getSkinnable();
+        MFXListView<T> listView = (MFXListView<T>) getSkinnable();
 
         listView.setOnMouseExited(event -> {
             if (listView.isHideScrollBars()) {
@@ -174,7 +174,7 @@ public class MFXLegacyListViewSkin<T> extends ListViewSkin<T> {
         });
     }
 
-    private void bindScrollBars(MFXLegacyListView<?> listView) {
+    private void bindScrollBars(MFXListView<?> listView) {
         final Set<Node> nodes = listView.lookupAll("VirtualScrollBar");
         for (Node node : nodes) {
             if (node instanceof ScrollBar) {

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java

@@ -28,7 +28,7 @@ import io.github.palexdev.materialfx.effects.RippleGenerator;
 import io.github.palexdev.materialfx.filter.IFilterable;
 import io.github.palexdev.materialfx.filter.MFXFilterDialog;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
-import io.github.palexdev.materialfx.selection.ITableSelectionModel;
+import io.github.palexdev.materialfx.selection.base.ITableSelectionModel;
 import io.github.palexdev.materialfx.utils.DragResizer;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import javafx.animation.KeyFrame;

+ 4 - 1
materialfx/src/main/java/module-info.java

@@ -3,6 +3,8 @@ module MaterialFX.materialfx.main {
     requires javafx.fxml;
     requires javafx.graphics;
     requires java.desktop;
+
+    requires reactfx;
     requires org.apache.logging.log4j;
 
     exports io.github.palexdev.materialfx;
@@ -12,15 +14,16 @@ module MaterialFX.materialfx.main {
     exports io.github.palexdev.materialfx.controls;
     exports io.github.palexdev.materialfx.controls.base;
     exports io.github.palexdev.materialfx.controls.cell;
-    exports io.github.palexdev.materialfx.controls.cell.legacy;
     exports io.github.palexdev.materialfx.controls.enums;
     exports io.github.palexdev.materialfx.controls.factories;
+    exports io.github.palexdev.materialfx.controls.flowless;
     exports io.github.palexdev.materialfx.controls.legacy;
     exports io.github.palexdev.materialfx.effects;
     exports io.github.palexdev.materialfx.filter;
     exports io.github.palexdev.materialfx.font;
     exports io.github.palexdev.materialfx.notifications;
     exports io.github.palexdev.materialfx.selection;
+    exports io.github.palexdev.materialfx.selection.base;
     exports io.github.palexdev.materialfx.skins;
     exports io.github.palexdev.materialfx.skins.legacy;
     exports io.github.palexdev.materialfx.utils;

+ 37 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-flowless-listcell.css

@@ -0,0 +1,37 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.mfx-list-cell {
+    -fx-background-color: white;
+    -fx-border-color: white;
+    -fx-padding: 5;
+}
+
+.mfx-list-cell:hover {
+    -fx-background-color: rgb(222, 255, 214);
+    -fx-border-color: rgb(222, 255, 214);
+}
+
+.mfx-list-cell:selected {
+    -fx-background-color: rgb(173, 255, 153);
+    -fx-border-color: rgb(173, 255, 153);
+}
+
+.mfx-list-cell .ripple-generator {
+    -mfx-ripple-color: rgb(141, 255, 112);
+}

+ 114 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-flowless-listview.css

@@ -0,0 +1,114 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     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 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 General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.mfx-list-view {
+    -mfx-track-color: rgb(230, 230, 230);
+    -mfx-thumb-color: rgb(137, 137, 137);
+    -mfx-thumb-hover-color: rgb(89, 88, 91);
+}
+
+.mfx-list-view .virtual-flow {
+    -fx-padding: 5;
+}
+
+/* Remove JavaFX crap */
+.scroll-bar,
+.scroll-bar .decrement-arrow,
+.scroll-bar .increment-arrow,
+.scroll-bar .decrement-button,
+.scroll-bar .increment-button {
+    -fx-pref-width: 0;
+    -fx-pref-height: 0;
+}
+
+
+.scroll-bar:horizontal .increment-button,
+.scroll-bar:horizontal .decrement-button {
+    -fx-background-color: transparent;
+    -fx-background-radius: 0.0em;
+    -fx-padding: 0.0 0.0 10.0 0.0;
+}
+
+.scroll-bar:vertical .increment-button,
+.scroll-bar:vertical .decrement-button {
+    -fx-background-color: transparent;
+    -fx-background-radius: 0.0em;
+    -fx-padding: 0.0 10.0 0.0 0.0;
+
+}
+
+.scroll-bar .increment-arrow,
+.scroll-bar .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.15em 0.0;
+}
+
+.scroll-bar:horizontal .increment-arrow,
+.scroll-bar:horizontal .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.0 0.05em;
+}
+
+.scroll-bar:vertical .increment-arrow,
+.scroll-bar:vertical .decrement-arrow {
+    -fx-shape: " ";
+    -fx-padding: 0.0 0.05em;
+}
+
+/* Customize ScrollBars */
+.scroll-bar:horizontal .track {
+    -fx-background-color: -mfx-track-color;
+    -fx-border-color: transparent;
+    -fx-background-radius: 2.0em;
+    -fx-border-radius: 2.0em;
+}
+
+.scroll-bar:vertical .track {
+    -fx-background-color: -mfx-track-color;
+    -fx-border-color: transparent;
+    -fx-background-radius: 2.0em;
+    -fx-border-radius: 2.0em;
+}
+
+.scroll-bar .decrement-arrow,
+.scroll-bar .increment-arrow {
+    -fx-pref-width: 0;
+    -fx-pref-height: 0;
+}
+
+.scroll-bar {
+    -fx-background-color: transparent;
+    -fx-pref-width: 12;
+    -fx-pref-height: 12;
+    -fx-padding: 5 0.5 5 0.5;
+}
+
+.scroll-bar:horizontal .thumb,
+.scroll-bar:vertical .thumb {
+    -fx-background-color: -mfx-thumb-color;
+    -fx-background-insets: 2.0, 0.0, 0.0;
+    -fx-background-radius: 2.0em;
+}
+
+.scroll-bar:horizontal .thumb:hover,
+.scroll-bar:vertical .thumb:hover {
+    -fx-background-color: -mfx-thumb-hover-color;
+    -fx-background-insets: 1.5, 0.0, 0.0;
+    -fx-background-radius: 2.0em;
+}
+

+ 0 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/legacy/mfx-listcell.css → materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listcell.css


+ 0 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/legacy/mfx-listview.css → materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-listview.css