From 04c5dbd6f0f732a9ec0915a0d360eecebd0a435e Mon Sep 17 00:00:00 2001
From: Jurgen <5031427+Jugen@users.noreply.github.com>
Date: Tue, 5 Aug 2025 12:41:23 +0200
Subject: [PATCH 1/4] Add item index to Cell creation
---
src/main/java/org/fxmisc/flowless/Cell.java | 13 ++++
.../org/fxmisc/flowless/CellListManager.java | 12 ++--
.../java/org/fxmisc/flowless/CellPool.java | 12 ++--
.../fxmisc/flowless/IndexedMappedList.java | 68 +++++++++++++++++++
.../java/org/fxmisc/flowless/VirtualFlow.java | 19 ++++--
5 files changed, 108 insertions(+), 16 deletions(-)
create mode 100644 src/main/java/org/fxmisc/flowless/IndexedMappedList.java
diff --git a/src/main/java/org/fxmisc/flowless/Cell.java b/src/main/java/org/fxmisc/flowless/Cell.java
index 662cd3b..a62179c 100644
--- a/src/main/java/org/fxmisc/flowless/Cell.java
+++ b/src/main/java/org/fxmisc/flowless/Cell.java
@@ -47,6 +47,19 @@ default void updateItem(T item) {
throw new UnsupportedOperationException();
}
+ /**
+ * 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.
+ *
+ *
The default implementation calls {@link #updateItem(Object)}
+ *
+ * @param item the new item to display
+ */
+ default void updateItem(Integer index, T item) {
+ updateItem(item);
+ }
+
/**
* Called to update index of a visible cell.
*
diff --git a/src/main/java/org/fxmisc/flowless/CellListManager.java b/src/main/java/org/fxmisc/flowless/CellListManager.java
index 5394466..681f117 100644
--- a/src/main/java/org/fxmisc/flowless/CellListManager.java
+++ b/src/main/java/org/fxmisc/flowless/CellListManager.java
@@ -1,7 +1,7 @@
package org.fxmisc.flowless;
import java.util.Optional;
-import java.util.function.Function;
+import java.util.function.BiFunction;
import javafx.collections.ObservableList;
import javafx.scene.Node;
@@ -31,10 +31,10 @@ final class CellListManager> {
public CellListManager(
Node owner,
ObservableList items,
- Function super T, ? extends C> cellFactory) {
+ BiFunction cellFactory) {
this.owner = owner;
this.cellPool = new CellPool<>(cellFactory);
- this.cells = LiveList.map(items, this::cellForItem).memoize();
+ this.cells = new IndexedMappedList<>(items, this::cellForItem).memoize();
this.presentCells = cells.memoizedItems();
this.cellNodes = presentCells.map(Cell::getNode);
this.presentCellsSubscription = presentCells.observeQuasiModifications(this::presentCellsChanged);
@@ -95,8 +95,8 @@ public void cropTo(int fromItem, int toItem) {
}
}
- private C cellForItem(T item) {
- C cell = cellPool.getCell(item);
+ private C cellForItem(Integer index, T item) {
+ C cell = cellPool.getCell(index, item);
// apply CSS when the cell is first added to the scene
Node node = cell.getNode();
@@ -132,7 +132,7 @@ private C cellForItem(T item) {
* 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.
- *
+ *
* So here we take over event-bubbling duties for ScrollEvent
* and push them ourselves directly to the given owner.
*/
diff --git a/src/main/java/org/fxmisc/flowless/CellPool.java b/src/main/java/org/fxmisc/flowless/CellPool.java
index d3cddf1..7151056 100644
--- a/src/main/java/org/fxmisc/flowless/CellPool.java
+++ b/src/main/java/org/fxmisc/flowless/CellPool.java
@@ -2,17 +2,17 @@
import java.util.LinkedList;
import java.util.Queue;
-import java.util.function.Function;
+import java.util.function.BiFunction;
/**
* 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> {
- private final Function super T, ? extends C> cellFactory;
+ private final BiFunction cellFactory;
private final Queue pool = new LinkedList<>();
- public CellPool(Function super T, ? extends C> cellFactory) {
+ public CellPool(BiFunction cellFactory) {
this.cellFactory = cellFactory;
}
@@ -20,12 +20,12 @@ public CellPool(Function super T, ? extends C> 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) {
+ public C getCell(Integer index, T item) {
C cell = pool.poll();
if(cell != null) {
- cell.updateItem(item);
+ cell.updateItem(index, item);
} else {
- cell = cellFactory.apply(item);
+ cell = cellFactory.apply(index, item);
}
return cell;
}
diff --git a/src/main/java/org/fxmisc/flowless/IndexedMappedList.java b/src/main/java/org/fxmisc/flowless/IndexedMappedList.java
new file mode 100644
index 0000000..90e5718
--- /dev/null
+++ b/src/main/java/org/fxmisc/flowless/IndexedMappedList.java
@@ -0,0 +1,68 @@
+package org.fxmisc.flowless;
+
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.reactfx.Subscription;
+import org.reactfx.collection.LiveList;
+import org.reactfx.collection.LiveListBase;
+import org.reactfx.collection.QuasiListChange;
+import org.reactfx.collection.QuasiListModification;
+import org.reactfx.collection.UnmodifiableByDefaultLiveList;
+import org.reactfx.util.Lists;
+
+import javafx.collections.ObservableList;
+
+public class IndexedMappedList extends LiveListBase implements UnmodifiableByDefaultLiveList
+{
+ private final ObservableList extends E> source;
+ private final BiFunction mapper;
+
+ public IndexedMappedList(ObservableList extends E> source,
+ BiFunction mapper) {
+ this.source = source;
+ this.mapper = mapper;
+ }
+
+ @Override
+ public F get(int index) {
+ return mapper.apply(index, source.get(index));
+ }
+
+ @Override
+ public int size() {
+ return source.size();
+ }
+
+ @Override
+ protected Subscription observeInputs() {
+ return LiveList.observeQuasiChanges(source, this::sourceChanged);
+ }
+
+ protected void sourceChanged(QuasiListChange extends E> change) {
+ notifyObservers(mappedChangeView(change));
+ }
+
+ private QuasiListChange mappedChangeView(QuasiListChange extends E> change) {
+ return () -> {
+ List extends QuasiListModification extends E>> mods = change.getModifications();
+ return Lists., QuasiListModification>mappedView(mods, mod -> new QuasiListModification<>() {
+
+ @Override
+ public int getFrom() {
+ return mod.getFrom();
+ }
+
+ @Override
+ public int getAddedSize() {
+ return mod.getAddedSize();
+ }
+
+ @Override
+ public List extends F> getRemoved() {
+ return Lists.mappedView(mod.getRemoved(), elem -> mapper.apply(mod.getFrom(), elem));
+ }
+ });
+ };
+ }
+}
diff --git a/src/main/java/org/fxmisc/flowless/VirtualFlow.java b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
index 3de50c9..faea974 100644
--- a/src/main/java/org/fxmisc/flowless/VirtualFlow.java
+++ b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
@@ -4,6 +4,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.function.BiFunction;
import java.util.function.Function;
import javafx.beans.property.ObjectProperty;
@@ -94,7 +95,7 @@ public static enum Gravity {
ObservableList items,
Function super T, ? extends C> cellFactory,
Gravity gravity) {
- return new VirtualFlow<>(items, cellFactory, new HorizontalHelper(), gravity);
+ return new VirtualFlow<>(items, (ndx,item) -> cellFactory.apply(item), new HorizontalHelper(), gravity);
}
/**
@@ -113,7 +114,17 @@ public static enum Gravity {
ObservableList items,
Function super T, ? extends C> cellFactory,
Gravity gravity) {
- return new VirtualFlow<>(items, cellFactory, new VerticalHelper(), gravity);
+ return new VirtualFlow<>(items, (ndx,item) -> cellFactory.apply(item), new VerticalHelper(), gravity);
+ }
+
+ /**
+ * Creates a viewport that lays out content vertically from top to bottom
+ * but with a cellFactory that also receives the item index.
+ */
+ public static > VirtualFlow createVertical(
+ ObservableList items,
+ BiFunction cellFactory) {
+ return new VirtualFlow<>(items, cellFactory, new VerticalHelper(), Gravity.FRONT);
}
private final ObservableList items;
@@ -162,13 +173,13 @@ public Var lengthOffsetEstimateProperty() {
private VirtualFlow(
ObservableList items,
- Function super T, ? extends C> cellFactory,
+ BiFunction cellFactory,
OrientationHelper orientation,
Gravity gravity) {
this.getStyleClass().add("virtual-flow");
this.items = items;
this.orientation = orientation;
- this.cellListManager = new CellListManager<>(this, items, cellFactory);
+ this.cellListManager = new CellListManager(this, items, cellFactory);
this.gravity.set(gravity);
MemoizationList cells = cellListManager.getLazyCellList();
this.sizeTracker = new SizeTracker(orientation, layoutBoundsProperty(), cells);
From 900a1841688425be95a7b095471ae07c2508fef1 Mon Sep 17 00:00:00 2001
From: Jurgen <5031427+Jugen@users.noreply.github.com>
Date: Tue, 5 Aug 2025 15:25:55 +0200
Subject: [PATCH 2/4] Added tests
---
.../java/org/fxmisc/flowless/VirtualFlow.java | 10 +++
.../flowless/CellCreationWithIndexTest.java | 86 +++++++++++++++++++
.../flowless/IndexedMappedListTest.java | 83 ++++++++++++++++++
3 files changed, 179 insertions(+)
create mode 100644 src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java
create mode 100644 src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java
diff --git a/src/main/java/org/fxmisc/flowless/VirtualFlow.java b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
index faea974..8f6e3dc 100644
--- a/src/main/java/org/fxmisc/flowless/VirtualFlow.java
+++ b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
@@ -98,6 +98,16 @@ public static enum Gravity {
return new VirtualFlow<>(items, (ndx,item) -> cellFactory.apply(item), new HorizontalHelper(), gravity);
}
+ /**
+ * Creates a viewport that lays out content horizontally from left to right
+ * but with a cellFactory that also receives the item index.
+ */
+ public static > VirtualFlow createHorizontal(
+ ObservableList items,
+ BiFunction cellFactory) {
+ return new VirtualFlow<>(items, cellFactory, new HorizontalHelper(), Gravity.FRONT);
+ }
+
/**
* Creates a viewport that lays out content vertically from top to bottom
*/
diff --git a/src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java b/src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java
new file mode 100644
index 0000000..157e098
--- /dev/null
+++ b/src/test/java/org/fxmisc/flowless/CellCreationWithIndexTest.java
@@ -0,0 +1,86 @@
+package org.fxmisc.flowless;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+public class CellCreationWithIndexTest extends FlowlessTestBase {
+
+ private ObservableList items;
+ private Counter cellCreations = new Counter();
+ private VirtualFlow flow;
+
+ @Override
+ public void start(Stage stage) {
+ // set up items
+ items = FXCollections.observableArrayList();
+ for(int i = 0; i < 20; ++i) {
+ items.addAll("red", "green", "blue", "purple");
+ }
+
+ // set up virtual flow
+ flow = VirtualFlow.createVertical(
+ items,
+ (index, color) -> {
+ cellCreations.inc();
+ Region reg = new Label( " "+ index +"\t"+ color );
+ reg.setPrefHeight(16.0);
+ reg.setStyle("-fx-background-color: " + color);
+ return Cell.wrapNode(reg);
+ });
+
+ StackPane stackPane = new StackPane();
+ // 25 cells (each 16px high) fit into the viewport
+ stackPane.getChildren().add(flow);
+ stage.setScene(new Scene(stackPane, 200, 400));
+ stage.show();
+ }
+
+ @Before
+ public void setup() {
+ cellCreations.reset();
+ }
+
+ @Test
+ public void updating_an_item_in_viewport_only_creates_cell_once() {
+ // update an item in the viewport
+ interact(() -> items.set(10, "yellow"));
+ assertEquals(1, cellCreations.getAndReset());
+ assertEquals("10", getCellText(10));
+ }
+
+ private String getCellText(int index) {
+ return ((Label) flow.getCell(index).getNode()).getText().substring(2,4);
+ }
+
+ @Test
+ public void updating_an_item_outside_viewport_does_not_create_cell() {
+ // update an item outside the viewport
+ interact(() -> items.set(30, "yellow"));
+ assertEquals(0, cellCreations.getAndReset());
+ }
+
+ @Test
+ public void deleting_an_item_in_viewport_only_creates_cell_once() {
+ // delete an item in the middle of the viewport
+ interact(() -> items.remove(12));
+ assertEquals(1, cellCreations.getAndReset());
+ }
+
+ @Test
+ public void adding_an_item_in_viewport_only_creates_cell_once() {
+ // add an item in the middle of the viewport
+ interact(() -> items.add(12, "yellow"));
+ assertEquals(1, cellCreations.getAndReset());
+ assertEquals("12", getCellText(12));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java b/src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java
new file mode 100644
index 0000000..2f58319
--- /dev/null
+++ b/src/test/java/org/fxmisc/flowless/IndexedMappedListTest.java
@@ -0,0 +1,83 @@
+package org.fxmisc.flowless;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.reactfx.collection.ListModification;
+import org.reactfx.collection.LiveList;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class IndexedMappedListTest
+{
+ @Test
+ public void testIndexedList() {
+ ObservableList strings = FXCollections.observableArrayList("1", "22", "333");
+ // Live map receives index,item and returns %d-%d index item
+ LiveList lengths = new IndexedMappedList<>(strings, (index, item) -> String.format("%d-%d", index, item.length()));
+ assertArrayEquals(new String[] {"0-1", "1-2", "2-3"}, lengths.stream().toArray());
+
+ List removed = new ArrayList<>();
+ List added = new ArrayList<>();
+ lengths.observeChanges(ch -> {
+ for(ListModification extends String> mod: ch.getModifications()) {
+ removed.addAll(mod.getRemoved());
+ added.addAll(mod.getAddedSubList());
+ }
+ });
+
+ // Set a value in the list and check changes
+ strings.set(1, "4444");
+ assertArrayEquals(new String[] {"0-1", "1-4", "2-3"}, lengths.stream().toArray());
+ assertEquals(Collections.singletonList("1-4"), added);
+ assertEquals(Collections.singletonList("1-2"), removed);
+
+ // Add an entry to the list and check changes
+ strings.add("7777777");
+ assertArrayEquals(new String[] {"0-1", "1-4", "2-3", "3-7"}, lengths.stream().toArray());
+ assertEquals(Arrays.asList("1-4", "3-7"), added);
+ assertEquals(Collections.singletonList("1-2"), removed);
+
+ // Remove an entry to the list and check changes (note that 3-7 becomes 2-7)
+ strings.remove(2);
+ assertArrayEquals(new String[] {"0-1", "1-4", "2-7"}, lengths.stream().toArray());
+ assertEquals(Arrays.asList("1-4", "3-7"), added);
+ assertEquals(Arrays.asList("1-2", "2-3"), removed);
+ }
+
+ @Test
+ public void testLazyIndexedList() {
+ ObservableList strings = FXCollections.observableArrayList("1", "22", "333");
+ IntegerProperty evaluationsCounter = new SimpleIntegerProperty(0);
+ LiveList lengths = new IndexedMappedList<>(strings, (index, elem) -> {
+ evaluationsCounter.set(evaluationsCounter.get() + 1);
+ return String.format("%d-%d", index, elem.length());
+ });
+
+ lengths.observeChanges(ch -> {});
+ strings.remove(1);
+
+ assertEquals(0, evaluationsCounter.get());
+
+ // Get the first element and the counter has increased
+ assertEquals("0-1", lengths.get(0));
+ assertEquals(1, evaluationsCounter.get());
+
+ // Get the second element, it will evaluate one item
+ assertEquals("1-3", lengths.get(1));
+ assertEquals(2, evaluationsCounter.get());
+
+ // Get again the first, it will reevaluate it
+ assertEquals("0-1", lengths.get(0));
+ assertEquals(3, evaluationsCounter.get());
+ }
+}
From a7487aecbcdc690dd29be86b0b0e333ae93ed3c7 Mon Sep 17 00:00:00 2001
From: Jurgen <5031427+Jugen@users.noreply.github.com>
Date: Wed, 6 Aug 2025 17:28:24 +0200
Subject: [PATCH 3/4] Changed method name and added comments
---
src/main/java/org/fxmisc/flowless/Cell.java | 15 ++++++++-------
src/main/java/org/fxmisc/flowless/CellPool.java | 7 ++++++-
2 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/src/main/java/org/fxmisc/flowless/Cell.java b/src/main/java/org/fxmisc/flowless/Cell.java
index a62179c..e1db7de 100644
--- a/src/main/java/org/fxmisc/flowless/Cell.java
+++ b/src/main/java/org/fxmisc/flowless/Cell.java
@@ -38,13 +38,13 @@ default boolean isReusable() {
* this method is called to display a different item. {@link #reset()}
* will have been called before a call to this method.
*
- * The default implementation throws
- * {@link UnsupportedOperationException}.
+ *
The default implementation calls {@link #updateItem(Object)}
*
+ * @param index the item's position in the VirtualFlow list
* @param item the new item to display
*/
- default void updateItem(T item) {
- throw new UnsupportedOperationException();
+ default void update(Integer index, T item) {
+ updateItem(item);
}
/**
@@ -52,12 +52,13 @@ default void updateItem(T item) {
* this method is called to display a different item. {@link #reset()}
* will have been called before a call to this method.
*
- *
The default implementation calls {@link #updateItem(Object)}
+ *
The default implementation throws
+ * {@link UnsupportedOperationException}.
*
* @param item the new item to display
*/
- default void updateItem(Integer index, T item) {
- updateItem(item);
+ default void updateItem(T item) {
+ throw new UnsupportedOperationException();
}
/**
diff --git a/src/main/java/org/fxmisc/flowless/CellPool.java b/src/main/java/org/fxmisc/flowless/CellPool.java
index 7151056..5978572 100644
--- a/src/main/java/org/fxmisc/flowless/CellPool.java
+++ b/src/main/java/org/fxmisc/flowless/CellPool.java
@@ -23,8 +23,13 @@ public CellPool(BiFunction cellFactory) {
public C getCell(Integer index, T item) {
C cell = pool.poll();
if(cell != null) {
- cell.updateItem(index, item);
+ // Note that update(index,item) invokes
+ // cell.updateItem(item) by default
+ cell.update(index, item);
} else {
+ // Note that cellFactory may just be a wrapper:
+ // (index, item) -> wrappedFactory.apply(item)
+ // See the various VirtualFlow creation methods
cell = cellFactory.apply(index, item);
}
return cell;
From 342e64a8f961cd1866ae9f191cae7e3c39ff9255 Mon Sep 17 00:00:00 2001
From: Jurgen <5031427+Jugen@users.noreply.github.com>
Date: Wed, 6 Aug 2025 17:31:11 +0200
Subject: [PATCH 4/4] Fixed white space
---
src/main/java/org/fxmisc/flowless/CellPool.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/org/fxmisc/flowless/CellPool.java b/src/main/java/org/fxmisc/flowless/CellPool.java
index 5978572..499b27c 100644
--- a/src/main/java/org/fxmisc/flowless/CellPool.java
+++ b/src/main/java/org/fxmisc/flowless/CellPool.java
@@ -24,7 +24,7 @@ public C getCell(Integer index, T item) {
C cell = pool.poll();
if(cell != null) {
// Note that update(index,item) invokes
- // cell.updateItem(item) by default
+ // cell.updateItem(item) by default
cell.update(index, item);
} else {
// Note that cellFactory may just be a wrapper: