From 3f56dc9c595fb2c7dd07796ef0f7abcf61aef54f Mon Sep 17 00:00:00 2001 From: extio-de Date: Sun, 4 Oct 2026 08:57:05 +0200 Subject: [PATCH] fix(ScrollArea): renderingBoIds concurrency and add immutable view --- .../extio/game_engine/menu/OptionsModule.java | 2 +- .../game_engine/menu/ThemeEditorModule.java | 2 +- .../renderer/container/ScrollArea.java | 45 +++++++++++++------ .../dialog/FileSelectionDialogModule.java | 2 +- .../dialog/FileSelectionMoveDialogModule.java | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/extio/game_engine/menu/OptionsModule.java b/src/main/java/de/extio/game_engine/menu/OptionsModule.java index a2cc7e7..ad03779 100644 --- a/src/main/java/de/extio/game_engine/menu/OptionsModule.java +++ b/src/main/java/de/extio/game_engine/menu/OptionsModule.java @@ -134,7 +134,7 @@ public class OptionsModule extends AbstractClientModule implements OptionsModule @Override public void refreshContent() { - for (final var boId : this.contentScrollArea.getRenderingBoIds().toArray(new String[0])) { + for (final var boId : this.contentScrollArea.getRenderingBoIds()) { this.contentScrollArea.removeRenderingBo(boId); } for (final var tab : this.tabs) { diff --git a/src/main/java/de/extio/game_engine/menu/ThemeEditorModule.java b/src/main/java/de/extio/game_engine/menu/ThemeEditorModule.java index 6d7b60a..542f9df 100644 --- a/src/main/java/de/extio/game_engine/menu/ThemeEditorModule.java +++ b/src/main/java/de/extio/game_engine/menu/ThemeEditorModule.java @@ -222,7 +222,7 @@ public class ThemeEditorModule extends AbstractClientModule implements OptionsTh private void setupEditor() { this.editorWindow.clearRenderingBos(); - for (final var boId : this.scrollArea.getRenderingBoIds().toArray(new String[0])) { + for (final var boId : this.scrollArea.getRenderingBoIds()) { this.scrollArea.removeRenderingBo(boId); } this.colorSlotByControlId.clear(); diff --git a/src/main/java/de/extio/game_engine/renderer/container/ScrollArea.java b/src/main/java/de/extio/game_engine/renderer/container/ScrollArea.java index fb8be0c..5ec1b8c 100644 --- a/src/main/java/de/extio/game_engine/renderer/container/ScrollArea.java +++ b/src/main/java/de/extio/game_engine/renderer/container/ScrollArea.java @@ -2,6 +2,7 @@ package de.extio.game_engine.renderer.container; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -40,16 +41,20 @@ public class ScrollArea implements WindowComponent { protected final MutableCoordI2 contentDimension = MutableCoordI2.create(); protected final Set renderingBoIds = Collections.synchronizedSet(new HashSet<>()); + + protected volatile Set renderingBoIdsView = null; + + protected volatile boolean renderingBoIdsViewDirty = true; protected final Area2 relativeArea = new Area2(ImmutableCoordI2.zero(), ImmutableCoordI2.one()); - protected Window parent; + protected volatile Window parent; - protected double scrollPositionVertical = 1.0; // 1.0 = top, 0.0 = bottom + protected volatile double scrollPositionVertical = 1.0; // 1.0 = top, 0.0 = bottom - protected double scrollPositionHorizontal = 0.0; // 0.0 = left, 1.0 = right - - protected int verticalScrollInputRevision = 0; + protected volatile double scrollPositionHorizontal = 0.0; // 0.0 = left, 1.0 = right + + protected volatile int verticalScrollInputRevision = 0; public ScrollArea(final RenderingBoPool renderingBoPool, final EventService eventService) { this.renderingBoPool = renderingBoPool; @@ -95,7 +100,7 @@ public class ScrollArea implements WindowComponent { return; } - final double scrollIncrement = Math.max(0.00001, Math.min(1.0, (double)this.relativeArea.getDimension().getY() / this.contentDimension.getY() / 14.0)); + final double scrollIncrement = Math.max(0.00001, Math.min(1.0, (double) this.relativeArea.getDimension().getY() / this.contentDimension.getY() / 14.0)); final double delta = event.getButton() == 4 ? scrollIncrement : -scrollIncrement; this.updateVerticalScrollPosition(this.scrollPositionVertical + delta, true); this.parent.draw(); @@ -169,15 +174,17 @@ public class ScrollArea implements WindowComponent { public void putRenderingBo(final RenderingBo renderingBo) { this.renderingBoIds.add(renderingBo.getId()); + this.renderingBoIdsViewDirty = true; this.parent.putRenderingBo(renderingBo); } - + public T getRenderingBo(final String renderingBoId, final Class type) { return this.parent.getRenderingBo(renderingBoId, type); } public void removeRenderingBo(final String renderingBoId) { this.renderingBoIds.remove(renderingBoId); + this.renderingBoIdsViewDirty = true; this.parent.removeRenderingBo(renderingBoId); } @@ -189,7 +196,7 @@ public class ScrollArea implements WindowComponent { public double getScrollPositionVertical() { return this.scrollPositionVertical; } - + public int getVerticalScrollOffset() { final int maxOffset = this.getMaxVerticalScrollOffset(); if (maxOffset <= 0) { @@ -197,15 +204,15 @@ public class ScrollArea implements WindowComponent { } return (int) ((1.0 - this.scrollPositionVertical) * maxOffset); } - + public int getMaxVerticalScrollOffset() { return Math.max(0, this.contentDimension.getY() - this.relativeArea.getDimension().getY() + SCROLLBAR_WIDTH_WITH_MARGIN); } - + public int getVerticalScrollInputRevision() { return this.verticalScrollInputRevision; } - + public void setVerticalScrollOffset(final int verticalScrollOffset) { final int maxOffset = this.getMaxVerticalScrollOffset(); if (maxOffset <= 0) { @@ -227,7 +234,7 @@ public class ScrollArea implements WindowComponent { this.updateVerticalScrollPosition(scrollPositionVertical, false); this.draw(); } - + protected void updateVerticalScrollPosition(final double scrollPositionVertical, final boolean userInput) { final double clampedScrollPositionVertical = Math.max(0.0, Math.min(1.0, scrollPositionVertical)); if (userInput && Double.compare(clampedScrollPositionVertical, this.scrollPositionVertical) != 0) { @@ -263,9 +270,21 @@ public class ScrollArea implements WindowComponent { this.relativeArea.setDimension(relativeArea.getDimension().toImmutableCoordI2()); } + /** + * Returns an immutable copy of the rendering BO IDs currently in this scroll area. + * + * @return an immutable {@link List} containing all rendering BO IDs; safe to iterate + * while the scroll area is being modified + */ @Override public Set getRenderingBoIds() { - return this.renderingBoIds; + synchronized (this.renderingBoIds) { + if (this.renderingBoIdsViewDirty) { + this.renderingBoIdsView = Collections.unmodifiableSet(new HashSet<>(this.renderingBoIds)); + this.renderingBoIdsViewDirty = false; + } + return this.renderingBoIdsView; + } } @Override diff --git a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java index 0365f66..af29590 100644 --- a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java +++ b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java @@ -692,7 +692,7 @@ public class FileSelectionDialogModule extends AbstractClientModule { } private void clearScrollArea() { - for (final var boId : this.itemsScrollArea.getRenderingBoIds().toArray(new String[0])) { + for (final var boId : this.itemsScrollArea.getRenderingBoIds()) { this.itemsScrollArea.removeRenderingBo(boId); } } diff --git a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionMoveDialogModule.java b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionMoveDialogModule.java index ae153c5..9fbeaad 100644 --- a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionMoveDialogModule.java +++ b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionMoveDialogModule.java @@ -357,7 +357,7 @@ public class FileSelectionMoveDialogModule extends AbstractClientModule { } private void clearScrollArea() { - for (final var boId : this.folderScrollArea.getRenderingBoIds().toArray(new String[0])) { + for (final var boId : this.folderScrollArea.getRenderingBoIds()) { this.folderScrollArea.removeRenderingBo(boId); } }