All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fxmisc.flowless.VirtualFlowContent Maven / Gradle / Ivy

There is a newer version: 0.7.3
Show newest version
package org.fxmisc.flowless;

import java.util.Optional;
import java.util.function.Function;

import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;

import org.fxmisc.flowless.VirtualFlow.Gravity;
import org.reactfx.collection.MemoizationList;
import org.reactfx.util.Lists;
import org.reactfx.value.Val;
import org.reactfx.value.Var;

class VirtualFlowContent> extends Region {
    private final ObservableList items;
    private final OrientationHelper orientation;
    private final CellListManager cellListManager;
    private final SizeTracker sizeTracker;
    private final CellPositioner cellPositioner;
    private final Navigator navigator;

    // non-negative
    private final ReadOnlyDoubleWrapper breadthOffset = new ReadOnlyDoubleWrapper(0.0);
    public ReadOnlyDoubleProperty breadthOffsetProperty() {
        return breadthOffset.getReadOnlyProperty();
    }

    public Val totalBreadthEstimateProperty() {
        return sizeTracker.maxCellBreadthProperty();
    }

    private final Val breadthPositionEstimate;
    public Var breadthPositionEstimateProperty() {
        return breadthPositionEstimate.asVar(this::setBreadthPosition);
    }

    private final Val lengthOffsetEstimate;

    private final Val lengthPositionEstimate;
    public Var lengthPositionEstimateProperty() {
        return lengthPositionEstimate.asVar(this::setLengthPosition);
    }

    VirtualFlowContent(
            ObservableList items,
            Function cellFactory,
            OrientationHelper orientation,
            Gravity gravity) {
        this.getStyleClass().add("virtual-flow-content");
        this.items = items;
        this.orientation = orientation;
        this.cellListManager = new CellListManager<>(items, cellFactory);
        MemoizationList cells = cellListManager.getLazyCellList();
        this.sizeTracker = new SizeTracker(orientation, layoutBoundsProperty(), cells);
        this.cellPositioner = new CellPositioner<>(cellListManager, orientation, sizeTracker);
        this.navigator = new Navigator<>(cellListManager, cellPositioner, orientation, gravity, sizeTracker);

        getChildren().add(navigator);
        clipProperty().bind(Val.map(
                layoutBoundsProperty(),
                b -> new Rectangle(b.getWidth(), b.getHeight())));


        // set up bindings

        breadthPositionEstimate = Val.combine(
                breadthOffset,
                sizeTracker.viewportBreadthProperty(),
                sizeTracker.maxCellBreadthProperty(),
                (off, vpBr, totalBr) -> offsetToScrollbarPosition(off.doubleValue(), vpBr, totalBr));

        lengthOffsetEstimate = sizeTracker.lengthOffsetEstimateProperty();

        lengthPositionEstimate = Val.combine(
                lengthOffsetEstimate,
                sizeTracker.viewportLengthProperty(),
                sizeTracker.totalLengthEstimateProperty(),
                (off, vpLen, totalLen) -> offsetToScrollbarPosition(off, vpLen, totalLen))
                .orElseConst(0.0);
    }

    public void dispose() {
        navigator.dispose();
        sizeTracker.dispose();
        cellListManager.dispose();
    }

    public C getCellFor(int itemIndex) {
        Lists.checkIndex(itemIndex, items.size());
        return cellPositioner.getSizedCell(itemIndex);
    }

    public Optional getCellIfVisible(int itemIndex) {
        return cellPositioner.getCellIfVisible(itemIndex);
    }

    public ObservableList visibleCells() {
        return cellListManager.getLazyCellList().memoizedItems();
    }

    public Val totalLengthEstimateProperty() {
        return sizeTracker.totalLengthEstimateProperty();
    }

    @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;
            }
        }

        orientation.relocate(navigator, -breadthOffset.get(), 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(breadthOffset.get() + deltaBreadth);
    }

    void scrollXBy(double deltaX) {
        orientation.scrollHorizontallyBy(this, deltaX);
    }

    void scrollYBy(double deltaY) {
        orientation.scrollVerticallyBy(this, deltaY);
    }

    void scrollXToPixel(double pixel) {
        orientation.scrollHorizontallyToPixel(this, pixel);
    }

    void scrollYToPixel(double pixel) {
        orientation.scrollVerticallyToPixel(this, pixel);
    }

    Val totalWidthEstimateProperty() {
        return orientation.widthEstimateProperty(this);
    }

    Val totalHeightEstimateProperty() {
        return orientation.heightEstimateProperty(this);
    }

    Val horizontalPositionProperty() {
        return orientation.horizontalPositionProperty(this);
    }

    Val verticalPositionProperty() {
        return orientation.verticalPositionProperty(this);
    }

    VirtualFlowHit hit(double x, double y) {
        double bOff = orientation.getX(x, y);
        double lOff = orientation.getY(x, y);

        bOff += breadthOffset.get();

        if(items.isEmpty()) {
            return orientation.hitAfterCells(bOff, lOff);
        }

        layout();

        int firstVisible = cellPositioner.getFirstVisibleIndex().getAsInt();
        firstVisible = navigator.fillBackwardFrom0(firstVisible, lOff);
        C firstCell = cellPositioner.getVisibleCell(firstVisible);

        int lastVisible = cellPositioner.getLastVisibleIndex().getAsInt();
        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");
        }
    }

    void show(double viewportOffset) {
        if(viewportOffset < 0) {
            navigator.scrollTargetPositionBy(viewportOffset);
        } else if(viewportOffset > sizeTracker.getViewportLength()) {
            navigator.scrollTargetPositionBy(viewportOffset - sizeTracker.getViewportLength());
        } else {
            // do nothing, offset already in the viewport
        }
    }

    void show(int itemIdx) {
        navigator.setTargetPosition(new MinDistanceTo(itemIdx));
    }

    void showAsFirst(int itemIdx) {
        navigator.setTargetPosition(new StartOffStart(itemIdx, 0.0));
    }

    void showAsLast(int itemIdx) {
        navigator.setTargetPosition(new EndOffEnd(itemIdx, 0.0));
    }

    void showAtOffset(int itemIdx, double offset) {
        navigator.setTargetPosition(new StartOffStart(itemIdx, offset));
    }

    void showRegion(int itemIndex, Bounds region) {
      navigator.showLengthRegion(itemIndex, orientation.minY(region), orientation.maxY(region));
      showBreadthRegion(orientation.minX(region), orientation.maxX(region));
    }

    private void showBreadthRegion(double fromX, double toX) {
        double bOff = breadthOffset.get();
        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);
        }
    }

    private void setLengthPosition(double pos) {
        setLengthOffset(lengthPositionToPixels(pos));
    }

    private void setBreadthPosition(double pos) {
        setBreadthOffset(breadthPositionToPixels(pos));
    }

    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.scrollTargetPositionBy(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 = breadthOffset.get();

        if(pixels > max) pixels = max;
        if(pixels < 0) pixels = 0;

        if(pixels != current) {
            breadthOffset.set(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));
        }
    }

    private double lengthPositionToPixels(double pos) {
        double total = totalLengthEstimateProperty().getOrElse(0.0);
        double length = sizeTracker.getViewportLength();
        return scrollbarPositionToOffset(pos, length, total);
    }

    private double breadthPositionToPixels(double pos) {
        double total = totalBreadthEstimateProperty().getValue();
        double breadth = sizeTracker.getViewportBreadth();
        return scrollbarPositionToOffset(pos, breadth, total);
    }

    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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy