diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Component.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Component.java index b1381e2e..8cf9d163 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Component.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Component.java @@ -110,6 +110,11 @@ public interface Component extends VirtualView { // TODO Needs documentation boolean shouldRender(IFContext context); + /** + * Updates this component. + */ + void update(); + /** * Checks if two components area intersects with each other. * diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java index 5c6211f5..87a1c24d 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java @@ -176,8 +176,15 @@ public void updated(@NotNull IFSlotRenderContext context) { @Override public void clear(@NotNull IFContext context) { ((IFRenderContext) context).getContainer().removeItem(getPosition()); + } + + @Override + public void update() { + if (isManagedExternally()) + throw new IllegalStateException( + "This component is externally managed by another component and cannot be updated directly"); - setVisible(false); + if (root instanceof IFContext) ((IFContext) root).updateComponent(this); } @Override @@ -193,6 +200,8 @@ public void clicked(@NotNull Component component, @NotNull IFSlotClickContext co @Override public boolean isVisible() { + if (root instanceof Component) return ((Component) root).isVisible() && isVisible; + return isVisible; } diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Pagination.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Pagination.java index 1b171781..759471f6 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Pagination.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/Pagination.java @@ -1,5 +1,9 @@ package me.devnatan.inventoryframework.component; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; import me.devnatan.inventoryframework.state.StateValue; import me.devnatan.inventoryframework.state.StateValueHost; import org.jetbrains.annotations.ApiStatus; @@ -128,20 +132,103 @@ public interface Pagination extends ComponentComposition, StateValue { char getLayoutTarget(); /** + * Lazy pagination usually have a {@link java.util.function.Function} as source provider and + * this provider is only called again to set the current internal source when an explicit + * update is called. This kind of pagination is used to replicate a static-like pagination since + * it can use a Function to provide some information to composite the source that'll be built + * but will never change again. + *

+ * So, when this method returns true the only way to update the current source as a + * whole is triggering an update somehow e.g. by calling {@link #update()}. + *

+ * Page switches will not trigger the source provider to re-apply the current internal source. + *

* This is an internal inventory-framework API that should not be used from outside of * this library. No compatibility guarantees are provided. */ @ApiStatus.Internal - boolean isDynamic(); + boolean isLazy(); /** - * If the pagination data is being loaded or not. + * Static pagination is a type of pagination that usually have a {@link java.util.Collection} or + * something else as source provider and this source provider is called only on first render to + * set the current source as a whole, and never called again on the entire Pagination lifecycle. *

- * Only changes if {@link #isDynamic()} is true. + * This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. + */ + @ApiStatus.Internal + boolean isStatic(); + + /** + * Computed pagination usually have a {@link java.util.function.Function}-like as source provider + * and this provider is called each time this component is updated or the page is changed so the + * current as a whole will always be the result of the source provider, regardless the {@link #currentPage() current page}. + *

+ * This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. + */ + @ApiStatus.Internal + boolean isComputed(); + + /** + * Asynchronous pagination can have a mix of all others {@link #isStatic() static}, {@link #isLazy() lazy} and {@link #isComputed() computed} + * types of pagination source, that is: + *

+ *

+ * {@link java.util.concurrent.CompletableFuture} is the type of the source provider or something + * that results in a CompletableFuture. Pagination source type is defined by the implementation. + *

+ * Internally asynchronous pagination also have a {@link #isLoading() loading state} that + * tracks the loading state of the future and changes based on future completion. + *

+ * This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. + */ + @ApiStatus.Internal + boolean isAsync(); + + /** + * If the pagination data is being loaded. * *

This API is experimental and is not subject to the general compatibility guarantees * such API may be changed or may be removed completely in any further release. + * + * @return Loading state of the pagination. + * Always false when {@link #isStatic()} is true. */ @ApiStatus.Experimental boolean isLoading(); + + /** + * Gets all elements in a given page index based of the specified source. + * + * @param index The page index. + * @param pageSize Number of elements that each page can have. + * @param pagesCount Pre-calculated total number of pages available (set zero if not available). + * @param src The source to split. + * @return All elements in a page. + * @throws IndexOutOfBoundsException If the specified index is {@code < 0} or + * exceeds the pages count. + */ + static List splitSourceForPage(int index, int pageSize, int pagesCount, List src) { + if (src.isEmpty()) return Collections.emptyList(); + + if (src.size() <= pageSize) return new ArrayList<>(src); + if (index < 0 || (pagesCount > 0 && index > pagesCount)) + throw new IndexOutOfBoundsException(String.format( + "Page index must be between the range of 0 and %d. Given: %d", pagesCount - 1, index)); + + final List contents = new LinkedList<>(); + final int base = index * pageSize; + int until = base + pageSize; + if (until > src.size()) until = src.size(); + + for (int i = base; i < until; i++) contents.add(src.get(i)); + + return contents; + } } diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/PaginationElementConsumer.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/PaginationValueConsumer.java similarity index 64% rename from inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/PaginationElementConsumer.java rename to inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/PaginationValueConsumer.java index db7432e4..8153c751 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/PaginationElementConsumer.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/PaginationValueConsumer.java @@ -3,14 +3,16 @@ import org.jetbrains.annotations.NotNull; /** - * Represents an pagination element mapping operation that accepts three arguments and returns no result. + * Represents a pagination element mapping operation that accepts three arguments and returns no result. + *

* This is a {@link FunctionalInterface functional interface} whose functional method is {@link #accept(Object, Object, int, Object)}. - * @param The type of the pagination context - * @param The builder used to build the paginated element + * + * @param The type of the pagination context + * @param The builder used to build the paginated element * @param The value that represents the current element being paginated */ @FunctionalInterface -public interface PaginationElementConsumer { +public interface PaginationValueConsumer { /** * Performs this operation on the given arguments. @@ -20,5 +22,5 @@ public interface PaginationElementConsumer { * @param index The index of the element being paginated in the pagination * @param value The value that represents the current element being paginated */ - void accept(@NotNull Context context, @NotNull Builder builder, int index, @NotNull V value); + void accept(@NotNull C context, @NotNull B builder, int index, @NotNull V value); } diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFContext.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFContext.java index 61c01aea..190c7a5c 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFContext.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFContext.java @@ -167,18 +167,6 @@ public interface IFContext extends VirtualView, StateValueHost { */ void update(); - /** - * Checks if a component positioned in a given index is marked for removal. - *

- * This is an internal inventory-framework API that should not be used from outside of - * this library. No compatibility guarantees are provided. - * - * @param componentIndex The index of the component to be checked if it's marked for removal. - * @return If the component in the specified index (if any) is marked for removal. - */ - @ApiStatus.Internal - boolean isMarkedForRemoval(int componentIndex); - /** * Data defined when a context is created, usually this is data set when the context is * opened for a viewer. diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFRenderContext.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFRenderContext.java index 6c06d549..d498d136 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFRenderContext.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFRenderContext.java @@ -54,4 +54,11 @@ public interface IFRenderContext extends IFConfinedContext { */ @NotNull ViewContainer getContainer(); + + /** + * This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. + */ + @ApiStatus.Internal + boolean isRendered(); } diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFUpdateContext.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFUpdateContext.java deleted file mode 100644 index c3027bec..00000000 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFUpdateContext.java +++ /dev/null @@ -1,3 +0,0 @@ -package me.devnatan.inventoryframework.context; - -public interface IFUpdateContext extends IFContext {} diff --git a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/PaginationImpl.java b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/PaginationImpl.java index f0709b7c..e1d07765 100644 --- a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/PaginationImpl.java +++ b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/PaginationImpl.java @@ -4,7 +4,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -27,25 +26,24 @@ @VisibleForTesting public class PaginationImpl extends AbstractStateValue implements Pagination, InteractionHandler { - private final List components = new LinkedList<>(); - private final @NotNull IFContext host; + private List components = new ArrayList<>(); + private final IFContext host; + private boolean visible; // --- User provided --- private final char layoutTarget; - private final @NotNull Object sourceProvider; - private final @NotNull PaginationElementFactory elementFactory; + private final Object sourceProvider; + private final PaginationElementFactory elementFactory; private final BiConsumer pageSwitchHandler; // --- Internal --- private int currPageIndex; - private final boolean dynamic; + private final boolean isLazy, isStatic, isComputed, isAsync; private boolean pageWasChanged; private boolean initialized; private int pagesCount; - /** - * The number of elements that each page can have. -1 means uninitialized. - */ + // Number of elements that each page can have. -1 means uninitialized. private int pageSize = -1; // Changes when dynamic data source is used and being loaded @@ -59,18 +57,18 @@ public class PaginationImpl extends AbstractStateValue implements Pagination, In */ private Function _srcFactory; - /** - * Current page source. Only {@code null} before first pagination render. - */ + // Current page source, null before first pagination render. private List currSource; public PaginationImpl( - @NotNull State state, - @NotNull IFContext host, + State state, + IFContext host, char layoutTarget, - @NotNull Object sourceProvider, - @NotNull PaginationElementFactory elementFactory, - BiConsumer pageSwitchHandler) { + Object sourceProvider, + PaginationElementFactory elementFactory, + BiConsumer pageSwitchHandler, + boolean isAsync, + boolean isComputed) { super(state); this.host = host; this.layoutTarget = layoutTarget; @@ -78,58 +76,76 @@ public PaginationImpl( this.elementFactory = elementFactory; this.pageSwitchHandler = pageSwitchHandler; this.currSource = convertSourceProvider(); - this.dynamic = !(sourceProvider instanceof Collection); + this.isComputed = isComputed; + this.isAsync = isAsync; + this.isStatic = sourceProvider instanceof Collection || isAsync; + this.isLazy = + !isStatic && !isComputed && (sourceProvider instanceof Function || sourceProvider instanceof Supplier); } /** * Tries to access and load the source to the current page. *

- * If this pagination {@link #isDynamic() is dynamic} it tries to get the current data source - * dynamically or asynchronously and waits for its completion. + * If this pagination {@link #isLazy() is lazy} it tries to get the current data source + * dynamically or asynchronously and waits for its completion. For static pagination it returns + * immediately with the source. *

- * For static pagination it returns immediately with the source. - *

- * On asynchronous pagination the source update job will be inherited by the user provided one. - *

- * When job gets done the {@link #currSource} is updated with the result of the computation. + * On asynchronous pagination the source update job will be inherited by the user provided one + * and when job gets done the {@link #currSource} is updated with the result of the computation. * * @return A CompletableFuture with the current pagination source as result. * @throws IllegalStateException In static pagination when the current source wasn't yet defined. */ - @SuppressWarnings("unchecked") private CompletableFuture> loadSourceForTheCurrentPage() { - // Static pagination we just get the current source here since it will be always the same - if (!isDynamic()) { - if (currSource == null) throw new IllegalStateException("User provided pagination source cannot be null"); - - if (!initialized) pagesCount = calculatePagesCount(currSource); + /* + * In lazy pagination **that was already initialized (already rendered before)** we must + * use the current data source as source of truth to ensure that page switching do not + * re-trigger pagination data factory since it will always return the source as a whole, + * the original one, and not the source for the switched page. + */ + final boolean reuseLazy = isLazy() && initialized; + + if ((isStatic() || reuseLazy) && !isComputed()) { + // For unknown reasons already initialized but source is null, external modification? + if (initialized && currSource == null) + throw new IllegalStateException("User provided pagination source cannot be null"); + else { + // Lazy pagination have pages count calculated on first render as a computed flow + if (!isLazy()) pagesCount = calculatePagesCount(currSource); + } - return CompletableFuture.completedFuture(splitSourceForPage(currPageIndex, currSource)); + return CompletableFuture.completedFuture( + Pagination.splitSourceForPage(currPageIndex, getPageSize(), getPagesCount(), currSource)); } - CompletableFuture> job = new CompletableFuture<>(); isLoading = true; simulateStateUpdate(); - final Object source = _srcFactory.apply(host); - if (source instanceof CompletableFuture) { - job = (CompletableFuture>) source; - } else { - // Here we are covering the dynamic rendering that's can be the usage of factories like - // `Supplier` and Function<..., List> so we just cast the result here - job.complete((List) source); - } - // TODO Do some error treatment here, even if we expect to the user to handle it - job.thenAccept(this::updateSource).whenComplete((result, exception) -> { + return createProvidedNewSource().handle((result, exception) -> { + updateSource(result); isLoading = false; simulateStateUpdate(); + + if (isLazy()) return Pagination.splitSourceForPage(currPageIndex, getPageSize(), getPagesCount(), result); + else return result; }); + } + + @SuppressWarnings("unchecked") + private CompletableFuture> createProvidedNewSource() { + CompletableFuture> job = new CompletableFuture<>(); + + final Object source = _srcFactory.apply(host); + if (isAsync()) job = (CompletableFuture>) source; + else if (isComputed() || isLazy()) job.complete((List) source); + else throw new IllegalArgumentException("Unhandled pagination source"); + return job; } /** - * Updates the current source and the number of availalbe pages count based on that source. + * Updates the current source and the number of available pages count based on that source. * * @param newSource The new data source. */ @@ -168,33 +184,6 @@ public int getPageSize() { return pageSize; } - /** - * Gets all elements in a given page index based on the current data source. - * - * @param index The page index. - * @param src The source to split. - * @return All elements in a page. - * @throws IndexOutOfBoundsException If the specified index is {@code < 0} or - * exceeds the {@link #getPagesCount() pages count}. - */ - private List splitSourceForPage(int index, List src) { - if (src.isEmpty()) return Collections.emptyList(); - - if (src.size() <= pageSize) return new ArrayList<>(src); - if (index < 0 || index > getPagesCount()) - throw new IndexOutOfBoundsException(String.format( - "Page index must be between the range of 0 and %d. Given: %d", getPagesCount() - 1, index)); - - final List contents = new LinkedList<>(); - final int base = index * pageSize; - int until = base + pageSize; - if (until > src.size()) until = src.size(); - - for (int i = base; i < until; i++) contents.add(src.get(i)); - - return contents; - } - /** * Loads pagination components using container boundaries, no constraints. *

@@ -205,13 +194,12 @@ private List splitSourceForPage(int index, List src) { * @param context The render context. * @param pageContents Elements of the current page. */ - private void loadComponentsForUnconstrainedPagination(IFRenderContext context, List pageContents) { + private void addComponentsForUnconstrainedPagination(IFRenderContext context, List pageContents) { final ViewContainer container = context.getContainer(); pageSize = container.getSize(); - final int lastSlot = container.getLastSlot(); - - for (int i = container.getFirstSlot(); i < Math.min(lastSlot + 1, pageContents.size()); i++) { + final int lastSlot = Math.min(container.getLastSlot() + 1 /* inclusive */, pageContents.size()); + for (int i = container.getFirstSlot(); i < lastSlot; i++) { final Object value = pageContents.get(i); final ComponentFactory factory = elementFactory.create(context, i, i, value); getComponentsInternal().add(factory.create()); @@ -227,12 +215,13 @@ private void loadComponentsForUnconstrainedPagination(IFRenderContext context, L * @param context The render context. * @param pageContents Elements of the current page. */ - private void loadComponentsForLayeredPagination(IFRenderContext context, List pageContents) { + private void addComponentsForLayeredPagination(IFRenderContext context, List pageContents) { if (pageContents.isEmpty()) return; + final LayoutSlot targetLayoutSlot = getLayoutSlotForCurrentTarget(context); final int elementsLen = pageContents.size(); int iterationIndex = 0; - for (final int position : getLayoutSlotForCurrentTarget(context).getPositions()) { + for (final int position : targetLayoutSlot.getPositions()) { final Object value = pageContents.get(iterationIndex++); final ComponentFactory factory = elementFactory.create(context, iterationIndex, position, value); final Component component = factory.create(); @@ -305,8 +294,8 @@ private List convertSourceProvider() { */ private CompletableFuture loadCurrentPage(IFRenderContext context) { return loadSourceForTheCurrentPage().thenAccept(pageContents -> { - if (context.getConfig().getLayout() != null) loadComponentsForLayeredPagination(context, pageContents); - else loadComponentsForUnconstrainedPagination(context, pageContents); + if (context.getConfig().getLayout() != null) addComponentsForLayeredPagination(context, pageContents); + else addComponentsForUnconstrainedPagination(context, pageContents); }); } @@ -343,7 +332,8 @@ public int getPosition() { @Override public void render(@NotNull IFSlotRenderContext context) { if (!initialized) { - final IFRenderContext root = (IFRenderContext) context.getParent(); + setVisible(true); + final IFRenderContext root = context.getParent(); updatePageSize(root); loadCurrentPage(root).thenRun(() -> renderChild(context)); initialized = true; @@ -359,15 +349,18 @@ private void renderChild(IFSlotRenderContext context) { @Override public void updated(@NotNull IFSlotRenderContext context) { + final IFRenderContext renderContext = context.getParent(); + // If page was changed all components will be removed, so don't trigger update on them if (pageWasChanged) { - final IFRenderContext renderContext = (IFRenderContext) context.getParent(); - clearChild(renderContext, true); + getComponentsInternal().forEach(child -> child.clear(renderContext)); + components = new ArrayList<>(); + getComponentsInternal().clear(); loadCurrentPage(renderContext).thenRun(() -> { render(context); simulateStateUpdate(); - pageWasChanged = false; }); + pageWasChanged = false; return; } @@ -386,22 +379,11 @@ private void simulateStateUpdate() { @Override public void clear(@NotNull IFContext context) { - // Only clear components if page was changed to not make the clear operation inconsistent if (!pageWasChanged) { getComponentsInternal().forEach(child -> child.clear(context)); return; } - clearChild(context, false); - } - - private void clearChild(IFContext context, boolean bulk) { - if (bulk) { - getComponentsInternal().forEach(child -> child.clear(context)); - getComponentsInternal().clear(); - return; - } - final Iterator childIterator = getComponentsInternal().iterator(); while (childIterator.hasNext()) { Component child = childIterator.next(); @@ -480,6 +462,7 @@ public boolean isLastPage() { @Override public boolean hasPage(int pageIndex) { + if (isComputed()) return true; if (pageIndex < 0) return false; return pageIndex < getPagesCount(); } @@ -525,8 +508,23 @@ public char getLayoutTarget() { } @Override - public boolean isDynamic() { - return dynamic; + public boolean isLazy() { + return isLazy; + } + + @Override + public boolean isStatic() { + return isStatic; + } + + @Override + public boolean isComputed() { + return isComputed; + } + + @Override + public boolean isAsync() { + return isAsync; } @Override @@ -542,31 +540,21 @@ public Iterator iterator() { @Override public boolean isVisible() { - for (final Component children : this) { - if (!children.isVisible()) return false; - } - - return true; + return visible; } @Override public void setVisible(boolean visible) { + this.visible = visible; getComponentsInternal().forEach(component -> component.setVisible(visible)); } @Override public void clicked(@NotNull Component component, @NotNull IFSlotClickContext context) { - final List components = getComponentsInternal(); - if (components.isEmpty()) return; - if (components.size() == 1) { - final Component child = components.get(0); - if (child.getInteractionHandler() != null && child.getInteractionHandler() != null) { - child.getInteractionHandler().clicked(component, context); - } - return; - } + // Lock child interactions while page is being updated + if (pageWasChanged) return; - for (final Component child : components) { + for (final Component child : getComponentsInternal()) { if (child.getInteractionHandler() == null) continue; if (child.isContainedWithin(context.getClickedSlot())) { child.getInteractionHandler().clicked(component, context); @@ -585,6 +573,11 @@ public boolean shouldRender(IFContext context) { return true; } + @Override + public void update() { + ((IFContext) getRoot()).updateComponent(this); + } + @VisibleForTesting List getComponentsInternal() { return components; @@ -598,7 +591,7 @@ public boolean equals(Object o) { return getLayoutTarget() == that.getLayoutTarget() && currPageIndex == that.currPageIndex && getPageSize() == that.getPageSize() - && isDynamic() == that.isDynamic() + && isLazy() == that.isLazy() && pageWasChanged == that.pageWasChanged && Objects.equals(sourceProvider, that.sourceProvider) && Objects.equals(pageSwitchHandler, that.pageSwitchHandler); @@ -612,7 +605,7 @@ public int hashCode() { pageSwitchHandler, currPageIndex, getPageSize(), - isDynamic(), + isLazy(), pageWasChanged); } @@ -626,7 +619,7 @@ public String toString() { + pageSwitchHandler + ", currPageIndex=" + currPageIndex + ", pageSize=" + pageSize + ", dynamic=" - + dynamic + ", pageWasChanged=" + + isLazy + ", pageWasChanged=" + pageWasChanged + ", _srcFactory=" + _srcFactory + ", currSource=" + currSource + "} " diff --git a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java index d753bf35..71c961b0 100644 --- a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java +++ b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java @@ -1,9 +1,7 @@ package me.devnatan.inventoryframework.context; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; -import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -23,7 +21,6 @@ abstract class AbstractIFContext extends DefaultStateValueHost implements IFContext { private final List components = new LinkedList<>(); - private final Deque markedForRemoval = new ArrayDeque<>(); private final Map indexedViewers = new HashMap<>(); protected ViewConfig config; @@ -83,7 +80,7 @@ public void removeComponent(@NotNull Component component) { } } - private IFSlotRenderContext createRenderContext(@NotNull Component component) { + private IFSlotRenderContext createSlotRenderContext(@NotNull Component component) { if (!(this instanceof IFRenderContext)) throw new InventoryFrameworkException("Slot render context cannot be created from non-render parent"); @@ -116,12 +113,12 @@ public void renderComponent(@NotNull Component component) { return; } - component.render(createRenderContext(component)); + component.render(createSlotRenderContext(component)); } @Override public void updateComponent(@NotNull Component component) { - component.updated(createRenderContext(component)); + component.updated(createSlotRenderContext(component)); } @Override @@ -129,11 +126,6 @@ public void update() { getRoot().getPipeline().execute(StandardPipelinePhases.UPDATE, this); } - @Override - public final boolean isMarkedForRemoval(int componentIndex) { - return markedForRemoval.contains(componentIndex); - } - @Override public final boolean isShared() { return getIndexedViewers().size() > 1; diff --git a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptor.java b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptor.java index 5d3fbfd9..3c2aab7e 100644 --- a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptor.java +++ b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptor.java @@ -3,7 +3,6 @@ import java.util.List; import me.devnatan.inventoryframework.VirtualView; import me.devnatan.inventoryframework.component.Component; -import me.devnatan.inventoryframework.context.IFContext; import me.devnatan.inventoryframework.context.IFRenderContext; /** @@ -15,11 +14,12 @@ public final class UpdateInterceptor implements PipelineInterceptor public void intercept(PipelineContext pipeline, VirtualView subject) { if (!(subject instanceof IFRenderContext)) return; - final IFContext context = (IFContext) subject; + final IFRenderContext context = (IFRenderContext) subject; + if (!context.isRendered()) return; + final List componentList = context.getComponents(); - for (int i = 0; i < componentList.size(); i++) { - final Component component = componentList.get(i); - if (context.isMarkedForRemoval(i)) { + for (final Component component : componentList) { + if (!component.isVisible()) { component.clear(context); continue; } diff --git a/inventory-framework-core/src/test/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptorTest.java b/inventory-framework-core/src/test/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptorTest.java index fb76ccc6..5bd0d8c5 100644 --- a/inventory-framework-core/src/test/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptorTest.java +++ b/inventory-framework-core/src/test/java/me/devnatan/inventoryframework/pipeline/UpdateInterceptorTest.java @@ -3,11 +3,9 @@ import static me.devnatan.inventoryframework.TestUtils.createContextMock; import static me.devnatan.inventoryframework.TestUtils.createRootMock; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -15,7 +13,6 @@ import me.devnatan.inventoryframework.RootView; import me.devnatan.inventoryframework.ViewContainer; import me.devnatan.inventoryframework.VirtualView; -import me.devnatan.inventoryframework.component.Component; import me.devnatan.inventoryframework.component.ItemComponent; import me.devnatan.inventoryframework.context.IFContext; import me.devnatan.inventoryframework.context.IFRenderContext; @@ -23,29 +20,6 @@ public class UpdateInterceptorTest { - @Test - void clearWhenMarkedForRemoval() { - Pipeline pipeline = new Pipeline<>(StandardPipelinePhases.UPDATE); - pipeline.intercept(StandardPipelinePhases.UPDATE, new UpdateInterceptor()); - - RootView root = createRootMock(); - IFContext context = createContextMock(root, IFRenderContext.class); - when(context.isShared()).thenReturn(true); - ViewContainer container = mock(ViewContainer.class); - when(context.getContainer()).thenReturn(container); - - Component component = mock(Component.class); - when(context.isMarkedForRemoval(anyInt())).thenReturn(true); - when(context.getComponents()).thenReturn(Collections.singletonList(component)); - when(root.getContexts()).thenReturn(Collections.singleton(context)); - - pipeline.execute(StandardPipelinePhases.UPDATE, context); - - verify(component, times(1)).clear(eq(context)); - verify(component, never()).updated(any()); - verify(component, never()).render(any()); - } - @Test void neverRenderIfItemDoNotHaveRenderHandler() { Pipeline pipeline = new Pipeline<>(StandardPipelinePhases.UPDATE); diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java index ef3b2629..9f65506a 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/IFViewFrame.java @@ -42,8 +42,7 @@ protected IFViewFrame() {} return getRegisteredViews().values().stream() .filter(view -> view.getClass().equals(type)) .findFirst() - .orElseThrow(() -> - new IllegalArgumentException(String.format("View not found or not registered: %s", type))); + .orElseThrow(() -> new IllegalArgumentException(String.format("View not registered: %s", type))); } /** diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PaginationStateBuilder.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PaginationStateBuilder.java index a1bc4d3c..9c72238c 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PaginationStateBuilder.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PaginationStateBuilder.java @@ -3,27 +3,30 @@ import java.util.function.BiConsumer; import me.devnatan.inventoryframework.component.*; import me.devnatan.inventoryframework.context.IFContext; -import me.devnatan.inventoryframework.context.IFSlotContext; import me.devnatan.inventoryframework.internal.LayoutSlot; import me.devnatan.inventoryframework.state.State; import org.jetbrains.annotations.NotNull; @SuppressWarnings("rawtypes") public final class PaginationStateBuilder< - Context extends IFContext, - SlotContext extends IFSlotContext, - Builder extends ItemComponentBuilder & ComponentFactory, - V> { + Context extends IFContext, Builder extends ItemComponentBuilder & ComponentFactory, V> { private final PlatformView root; private final Object sourceProvider; private char layoutTarget = LayoutSlot.FILLED_RESERVED_CHAR; private PaginationElementFactory elementFactory; private BiConsumer pageSwitchHandler; + private final boolean async, computed; PaginationStateBuilder(PlatformView root, Object sourceProvider) { + this(root, sourceProvider, false, false); + } + + PaginationStateBuilder(PlatformView root, Object sourceProvider, boolean async, boolean computed) { this.root = root; this.sourceProvider = sourceProvider; + this.async = async; + this.computed = computed; } /** @@ -38,9 +41,8 @@ public final class PaginationStateBuilder< * @param itemFactory The item factory. * @return This pagination state builder. */ - public PaginationStateBuilder itemFactory( - @NotNull BiConsumer itemFactory) { - return itemFactory(((context, builder, index, value) -> itemFactory.accept(builder, value))); + public PaginationStateBuilder itemFactory(@NotNull BiConsumer itemFactory) { + return elementFactory(((context, builder, index, value) -> itemFactory.accept(builder, value))); } /** @@ -52,16 +54,16 @@ public PaginationStateBuilder itemFactory( *

* This function is called for every single paginated element. * - * @param itemFactory The item factory. + * @param elementConsumer The element consumer. * @return This pagination state builder. */ - public PaginationStateBuilder itemFactory( - @NotNull PaginationElementConsumer itemFactory) { + public PaginationStateBuilder elementFactory( + @NotNull PaginationValueConsumer elementConsumer) { this.elementFactory = (context, index, slot, value) -> { @SuppressWarnings("unchecked") Builder builder = (Builder) root.getElementFactory().createComponentBuilder(context); builder.withSlot(slot).withExternallyManaged(true); - itemFactory.accept(context, builder, index, value); + elementConsumer.accept(context, builder, index, value); return builder; }; return this; @@ -79,7 +81,7 @@ public PaginationStateBuilder itemFactory( * @param layoutTarget The target layout character. * @return This pagination state builder. */ - public PaginationStateBuilder layoutTarget(char layoutTarget) { + public PaginationStateBuilder layoutTarget(char layoutTarget) { this.layoutTarget = layoutTarget; return this; } @@ -93,7 +95,7 @@ public PaginationStateBuilder layoutTarget(cha * @param pageSwitchHandler The page switch handler. * @return This pagination state builder. */ - public PaginationStateBuilder onPageSwitch( + public PaginationStateBuilder onPageSwitch( @NotNull BiConsumer pageSwitchHandler) { this.pageSwitchHandler = pageSwitchHandler; return this; @@ -111,7 +113,7 @@ public State build() { "Element factory from #buildPaginationState(...) cannot be null. Set it using %s or %s.", "#elementFactory(PaginationElementFactory)", "#itemFactory(BiConsumer)")); - return root.buildPaginationState(this); + return root.createPaginationState(this); } PlatformView getRoot() { @@ -133,4 +135,12 @@ PaginationElementFactory getElementFactory() { BiConsumer getPageSwitchHandler() { return pageSwitchHandler; } + + boolean isAsync() { + return async; + } + + boolean isComputed() { + return computed; + } } diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java index ec0a1947..6f7cad0b 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/PlatformView.java @@ -17,6 +17,7 @@ import me.devnatan.inventoryframework.context.IFSlotClickContext; import me.devnatan.inventoryframework.context.IFSlotContext; import me.devnatan.inventoryframework.context.PlatformContext; +import me.devnatan.inventoryframework.context.PlatformRenderContext; import me.devnatan.inventoryframework.internal.ElementFactory; import me.devnatan.inventoryframework.internal.PlatformUtils; import me.devnatan.inventoryframework.pipeline.AvailableSlotInterceptor; @@ -252,6 +253,7 @@ public void removeContext(@NotNull TContext context) { @ApiStatus.Internal public void renderContext(@NotNull TRenderContext context) { getPipeline().execute(context); + ((PlatformRenderContext) context).setRendered(); @SuppressWarnings("rawtypes") final PlatformView view = (PlatformView) context.getRoot(); @@ -516,184 +518,181 @@ protected final State initialState() { } /** - * Creates a new unmodifiable static pagination state. + * Creates a new immutable pagination with static data source. * * @param sourceProvider The data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for + * @param elementConsumer The function for creating pagination items, this function is called for * each paged element (item) on a page. * @param The pagination data type. * @return A new immutable pagination state. */ protected final State paginationState( - @NotNull List sourceProvider, @NotNull BiConsumer itemFactory) { + @NotNull List sourceProvider, + @NotNull PaginationValueConsumer elementConsumer) { return this.buildPaginationState(sourceProvider) - .itemFactory(itemFactory) + .elementFactory(elementConsumer) .build(); } /** - * Creates a new unmodifiable static pagination state. + * Creates a new unmodifiable computed pagination state. * - * @param sourceProvider The data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for + * @param sourceProvider Data source for pagination. + * @param valueConsumer Function for creating pagination items, this function is called for * each paged element (item) on a page. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new unmodifiable pagination state. */ - protected final State paginationState( - @NotNull List sourceProvider, - @NotNull PaginationElementConsumer itemFactory) { - return this.buildPaginationState(sourceProvider) - .itemFactory(itemFactory) + protected final State computedPaginationState( + @NotNull Function> sourceProvider, + @NotNull PaginationValueConsumer valueConsumer) { + return this.buildComputedPaginationState(sourceProvider) + .elementFactory(valueConsumer) .build(); } /** - * Creates a new unmodifiable dynamic pagination state. + * Creates a new unmodifiable computed pagination state with asynchronous data source. + *

+ * This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. * * @param sourceProvider The data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for + * @param valueConsumer The function for creating pagination items, this function is called for * each paged element (item) on a page. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new unmodifiable pagination state. */ - protected final State paginationState( - @NotNull Function> sourceProvider, @NotNull BiConsumer itemFactory) { - return this.buildPaginationState(sourceProvider) - .itemFactory(itemFactory) + @ApiStatus.Experimental + protected final State computedAsyncPaginationState( + @NotNull Function>> sourceProvider, + @NotNull PaginationValueConsumer valueConsumer) { + return this.buildComputedAsyncPaginationState(sourceProvider) + .elementFactory(valueConsumer) .build(); } /** - * Creates a new unmodifiable dynamic pagination state. + * Creates a new unmodifiable lazy pagination state. * - * @param sourceProvider The data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for + * @param sourceProvider Data source for pagination. + * @param valueConsumer Function for creating pagination items, this function is called for * each paged element (item) on a page. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new unmodifiable pagination state. */ - protected final State paginationState( + protected final State lazyPaginationState( @NotNull Function> sourceProvider, - @NotNull PaginationElementConsumer itemFactory) { - return this.buildPaginationState(sourceProvider) - .itemFactory(itemFactory) + @NotNull PaginationValueConsumer valueConsumer) { + return this.buildLazyPaginationState(sourceProvider) + .elementFactory(valueConsumer) .build(); } /** - * Creates a new unmodifiable dynamic pagination state. + * Creates a new unmodifiable lazy pagination state. * - * @param sourceProvider The data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for + * @param sourceProvider Data source for pagination. + * @param valueConsumer Function for creating pagination items, this function is called for * each paged element (item) on a page. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new unmodifiable pagination state. */ - protected final State paginationState( - @NotNull Supplier> sourceProvider, @NotNull BiConsumer itemFactory) { - return this.buildPaginationState(sourceProvider) - .itemFactory(itemFactory) + protected final State lazyPaginationState( + @NotNull Supplier> sourceProvider, + @NotNull PaginationValueConsumer valueConsumer) { + return this.buildLazyPaginationState(sourceProvider) + .elementFactory(valueConsumer) .build(); } /** - * Creates a new unmodifiable dynamic pagination state. + * Creates a new unmodifiable lazy pagination state with asynchronous data source. + *

+ * This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. * * @param sourceProvider The data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for + * @param valueConsumer The function for creating pagination items, this function is called for * each paged element (item) on a page. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new unmodifiable pagination state. */ - protected final State paginationState( - @NotNull Supplier> sourceProvider, - @NotNull PaginationElementConsumer itemFactory) { - return this.buildPaginationState(sourceProvider) - .itemFactory(itemFactory) + @ApiStatus.Experimental + protected final State lazyAsyncPaginationState( + @NotNull Function>> sourceProvider, + @NotNull PaginationValueConsumer valueConsumer) { + return this.buildLazyAsyncPaginationState(sourceProvider) + .elementFactory(valueConsumer) .build(); } /** - * Creates a new unmodifiable asynchronous pagination state. - *

- * This API is experimental and is not subject to the general compatibility guarantees - * such API may be changed or may be removed completely in any further release. + * Creates a new unmodifiable static pagination state builder. * - * @param sourceProvider The asynchronous data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for - * each paged element (item) on a page. + * @param sourceProvider The data source for pagination. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new pagination state builder. */ - @ApiStatus.Experimental - protected final State asyncPaginationState( - @NotNull Function>> sourceProvider, - @NotNull BiConsumer itemFactory) { - return this.buildAsyncPaginationState(sourceProvider) - .itemFactory(itemFactory) - .build(); + protected final PaginationStateBuilder buildPaginationState( + @NotNull List sourceProvider) { + return new PaginationStateBuilder<>(this, sourceProvider); } /** - * Creates a new unmodifiable asynchronous pagination state. - *

- * This API is experimental and is not subject to the general compatibility guarantees - * such API may be changed or may be removed completely in any further release. + * Creates a new unmodifiable dynamic pagination state builder. * - * @param sourceProvider The asynchronous data source for pagination. - * @param itemFactory The function for creating pagination items, this function is called for - * each paged element (item) on a page. + * @param sourceProvider The data source for pagination. * @param The pagination data type. - * @return A new immutable pagination state. + * @return A new pagination state builder. */ - @ApiStatus.Experimental - protected final State asyncPaginationState( - @NotNull Function>> sourceProvider, - @NotNull PaginationElementConsumer itemFactory) { - return this.buildAsyncPaginationState(sourceProvider) - .itemFactory(itemFactory) - .build(); + protected final PaginationStateBuilder buildComputedPaginationState( + @NotNull Function> sourceProvider) { + return new PaginationStateBuilder<>(this, sourceProvider, false, true); } /** - * Creates a new unmodifiable static pagination state builder. + * Creates a new unmodifiable computed pagination state builder with asynchronous data source. + *

+ * This API is experimental and is not subject to the general compatibility guarantees + * such API may be changed or may be removed completely in any further release. * * @param sourceProvider The data source for pagination. * @param The pagination data type. * @return A new pagination state builder. */ - protected final PaginationStateBuilder buildPaginationState( - @NotNull List sourceProvider) { - return new PaginationStateBuilder<>(this, sourceProvider); + @ApiStatus.Experimental + protected final PaginationStateBuilder buildComputedAsyncPaginationState( + @NotNull Function>> sourceProvider) { + return new PaginationStateBuilder<>(this, sourceProvider, true, true); } /** - * Creates a new unmodifiable dynamic pagination state builder. + * Creates a new unmodifiable lazy pagination state builder. * * @param sourceProvider The data source for pagination. * @param The pagination data type. * @return A new pagination state builder. */ - protected final PaginationStateBuilder buildPaginationState( + protected final PaginationStateBuilder buildLazyPaginationState( @NotNull Supplier> sourceProvider) { - return new PaginationStateBuilder<>(this, sourceProvider); + return new PaginationStateBuilder<>(this, sourceProvider, false, false); } /** - * Creates a new unmodifiable dynamic pagination state builder. + * Creates a new unmodifiable lazy pagination state builder. * * @param sourceProvider The data source for pagination. * @param The pagination data type. * @return A new pagination state builder. */ - protected final PaginationStateBuilder buildPaginationState( + protected final PaginationStateBuilder buildLazyPaginationState( @NotNull Function> sourceProvider) { - return new PaginationStateBuilder<>(this, sourceProvider); + return new PaginationStateBuilder<>(this, sourceProvider, false, false); } /** - * Creates a new unmodifiable asynchronous pagination state builder. + * Creates a new unmodifiable lazy pagination state builder with asynchronous data source. *

* This API is experimental and is not subject to the general compatibility guarantees * such API may be changed or may be removed completely in any further release. @@ -703,23 +702,24 @@ protected final PaginationStateBuilder PaginationStateBuilder buildAsyncPaginationState( + protected final PaginationStateBuilder buildLazyAsyncPaginationState( @NotNull Function>> sourceProvider) { - return new PaginationStateBuilder<>(this, sourceProvider); + return new PaginationStateBuilder<>(this, sourceProvider, true, false); } - final State buildPaginationState( - @NotNull PaginationStateBuilder builder) { + final State createPaginationState(@NotNull PaginationStateBuilder builder) { requireNotInitialized(); final long id = State.next(); @SuppressWarnings("unchecked") final StateValueFactory factory = (host, state) -> new PaginationImpl( state, - (TContext) host, + (IFContext) host, builder.getLayoutTarget(), builder.getSourceProvider(), (PaginationElementFactory) builder.getElementFactory(), - (BiConsumer) builder.getPageSwitchHandler()); + (BiConsumer) builder.getPageSwitchHandler(), + builder.isAsync(), + builder.isComputed()); final State state = new PaginationState(id, factory); stateRegistry.registerState(state, this); diff --git a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformRenderContext.java b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformRenderContext.java index d931ad2b..44578fa0 100644 --- a/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformRenderContext.java +++ b/inventory-framework-platform/src/main/java/me/devnatan/inventoryframework/context/PlatformRenderContext.java @@ -37,6 +37,7 @@ public abstract class PlatformRenderContext // --- Inherited --- private final ViewContainer container; + private boolean rendered; // --- Properties --- private final List componentBuilders = new ArrayList<>(); @@ -326,4 +327,18 @@ public final void resetTitleForPlayer() { tryThrowDoNotWorkWithSharedContext("resetTitleForEveryone()"); super.resetTitleForPlayer(); } + + @Override + public final boolean isRendered() { + return rendered; + } + + /** + * This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. + */ + @ApiStatus.Internal + public final void setRendered() { + this.rendered = true; + } } diff --git a/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/component/FakeComponent.java b/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/component/FakeComponent.java index 4a96210b..a13e5b9b 100644 --- a/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/component/FakeComponent.java +++ b/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/component/FakeComponent.java @@ -87,6 +87,9 @@ public boolean shouldRender(IFContext context) { return false; } + @Override + public void update() {} + @Override public void clicked(@NotNull Component component, @NotNull IFSlotClickContext context) {} }