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

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

The newest version!
package org.fxmisc.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 {
    static 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);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy