diff --git a/src/main/java/de/extio/game_engine/renderer/container/Container.java b/src/main/java/de/extio/game_engine/renderer/container/Container.java deleted file mode 100644 index 39ed55b..0000000 --- a/src/main/java/de/extio/game_engine/renderer/container/Container.java +++ /dev/null @@ -1,468 +0,0 @@ -package de.extio.game_engine.renderer.container; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; - -import de.extio.game_engine.event.EventService; -import de.extio.game_engine.module.AbstractClientModule; -import de.extio.game_engine.module.ModuleService; -import de.extio.game_engine.renderer.RendererControl; -import de.extio.game_engine.renderer.model.RenderingBo; -import de.extio.game_engine.renderer.model.bo.HorizontalAlignment; -import de.extio.game_engine.renderer.model.bo.VerticalAlignment; -import de.extio.game_engine.renderer.model.event.MouseClickEvent; -import de.extio.game_engine.renderer.model.event.MouseEvent; -import de.extio.game_engine.renderer.model.event.MouseMoveEvent; -import de.extio.game_engine.renderer.model.event.ViewportResizeEvent; -import de.extio.game_engine.renderer.work.RendererWorkingSet; -import de.extio.game_engine.renderer.work.RenderingBoPool; -import de.extio.game_engine.spatial2.SpatialUtils2; -import de.extio.game_engine.spatial2.model.Area2; -import de.extio.game_engine.spatial2.model.CoordI2; -import de.extio.game_engine.spatial2.model.ImmutableCoordI2; -import de.extio.game_engine.spatial2.model.MutableCoordI2; - -@Component -@Scope("prototype") -public class Container extends AbstractClientModule implements InitializingBean { - - protected final static List DISPLAYED_CONTAINERS = Collections.synchronizedList(new ArrayList<>()); - - protected final ModuleService moduleService; - - protected final RenderingBoPool renderingBoPool; - - protected final RendererControl rendererControl; - - protected final RendererWorkingSet rendererWorkingSet; - - protected final EventService eventService; - - protected final Area2 area = new Area2(MutableCoordI2.create(), MutableCoordI2.create()); - - // Normalized area of this container relative to reference resolution, the actual position is automatically aligned based on alignment settings (e.g. if the user uses ultra-wide monitors). Global scale factors are not considered/handled by the renderer control. - protected final Area2 normalizedArea = new Area2(MutableCoordI2.create(), MutableCoordI2.create()); - - protected boolean draggable; - - protected boolean dragging; - - protected CoordI2 dragPrevPosition; - - protected HorizontalAlignment horizontalAlignment = HorizontalAlignment.CENTER; - - protected VerticalAlignment verticalAlignment = VerticalAlignment.CENTER; - - protected short zIndex; - - protected Container parent; - - protected Set children = new HashSet<>(); - - public Container(final ModuleService moduleService, final RenderingBoPool renderingBoPool, final RendererControl rendererControl, final EventService eventService, final RendererWorkingSet rendererWorkingSet) { - this.moduleService = moduleService; - this.renderingBoPool = renderingBoPool; - this.rendererControl = rendererControl; - this.eventService = eventService; - this.rendererWorkingSet = rendererWorkingSet; - } - - @Override - public void afterPropertiesSet() throws Exception { - this.moduleService.loadModule(this); - } - - @Override - public void onActivate() { - this.eventService.register(ViewportResizeEvent.class, this.id, this::onViewportResize); - this.eventService.register(MouseMoveEvent.class, this.id, this::onMouseMoveEvent); - this.eventService.register(MouseClickEvent.class, this.id, this::onMouseClickEvent); - this.onViewportResize(null); - } - - @Override - public void onShow() { - DISPLAYED_CONTAINERS.add(this); - this.zIndexToTop(); - this.draw(); - } - - @Override - public void onHide() { - DISPLAYED_CONTAINERS.remove(this); - } - - protected void onViewportResize(final ViewportResizeEvent event) { - this.setNormalizedPosition(this.normalizedArea.getPosition()); - this.setNormalizedDimension(this.normalizedArea.getDimension()); - this.adjustIfOutsideViewport(); - this.draw(); - } - - private boolean adjustIfOutsideViewport() { - final var viewportDimension = this.rendererControl.getEffectiveViewportDimension(); - final var position = this.area.getPosition(); - final var dimension = this.area.getDimension(); - - final var containerArea = this.area.getDimension().getX() * this.area.getDimension().getY(); - final var intersectionArea = this.calculateIntersectionArea(position, dimension, viewportDimension); - final var intersectionPercentage = containerArea > 0 ? (double) intersectionArea / containerArea : 0.0; - - if (intersectionPercentage >= 0.1) { - return false; - } - - int adjustedX = position.getX(); - int adjustedY = position.getY(); - - if (position.getX() >= viewportDimension.getX()) { - adjustedX = viewportDimension.getX() - dimension.getX(); - } - else if (position.getX() + dimension.getX() <= 0) { - adjustedX = 0; - } - else if (position.getX() < 0) { - adjustedX = 0; - } - else if (position.getX() + dimension.getX() > viewportDimension.getX()) { - adjustedX = viewportDimension.getX() - dimension.getX(); - } - - if (position.getY() >= viewportDimension.getY()) { - adjustedY = viewportDimension.getY() - dimension.getY(); - } - else if (position.getY() + dimension.getY() <= 0) { - adjustedY = 0; - } - else if (position.getY() < 0) { - adjustedY = 0; - } - else if (position.getY() + dimension.getY() > viewportDimension.getY()) { - adjustedY = viewportDimension.getY() - dimension.getY(); - } - - this.normalizedArea.getPosition().setXY(adjustedX, adjustedY); - this.area.getPosition().setXY(adjustedX, adjustedY); - - this.LOGGER.debug("Adjusted container {} position to {},{} to remain visible in viewport", this.id, adjustedX, adjustedY); - return true; - } - - private int calculateIntersectionArea(final CoordI2 position, final CoordI2 dimension, final CoordI2 viewportDimension) { - final int x1 = Math.max(0, position.getX()); - final int y1 = Math.max(0, position.getY()); - final int x2 = Math.min(viewportDimension.getX(), position.getX() + dimension.getX()); - final int y2 = Math.min(viewportDimension.getY(), position.getY() + dimension.getY()); - - final int width = Math.max(0, x2 - x1); - final int height = Math.max(0, y2 - y1); - - return width * height; - } - - protected void onMouseMoveEvent(final MouseMoveEvent event) { - if (this.draggable && event.isDrag()) { - final Container containerUnderCursor = getContainerUnderCursor(event); - if (containerUnderCursor == this) { - if (!this.dragging) { - this.dragging = true; - this.dragPrevPosition = null; - this.zIndexToTop(); - this.LOGGER.debug("Started dragging container {}", this.id); - } - if (this.dragPrevPosition != null) { - final var delta = event.getScaledCoord().substract(this.dragPrevPosition); - if (!ImmutableCoordI2.zero().equals(delta)) { - this.setNormalizedPosition(this.getNormalizedPosition().add(delta)); - } - } - this.dragPrevPosition = event.getScaledCoord(); - } - } - } - - protected void onMouseClickEvent(final MouseClickEvent event) { - final Container containerUnderCursor = this.getContainerUnderCursor(event); - if (containerUnderCursor == this) { - if (event.isPressed() && !this.dragging) { - this.zIndexToTop(); - } - } - - if (!event.isPressed() && this.dragging) { - this.dragging = false; - this.dragPrevPosition = null; - if (this.adjustIfOutsideViewport()) { - this.draw(); - } - this.LOGGER.debug("Stopped dragging container {}", this.id); - } - } - - /** - * Override in subclasses, but make sure to call super.draw() at the end if overridden - */ - public void draw() { - final var uncommitted = this.rendererWorkingSet.getUncommittedWork(this.id); - for (final RenderingBo bo : uncommitted.values()) { - bo - .setZIndex(this.zIndex) - .withPositionAbsoluteAnchorTopLeft(this.area.getPosition() - .toMutableCoordD2() - .add(bo.getLocalX(), bo.getLocalY()) - .toImmutableCoordI2()); - this.rendererWorkingSet.put(this.id, bo); - } - - this.rendererWorkingSet.commit(this.id, true); - } - - public void putRenderingBo(final RenderingBo renderingBo) { - this.rendererWorkingSet.put(this.id, renderingBo); - } - - public void removeRenderingBo(final String renderingBoId) { - this.rendererWorkingSet.remove(this.id, renderingBoId); - } - - public void clearRenderingBos() { - this.rendererWorkingSet.clearNext(this.id); - } - - public void setNormalizedPosition(final CoordI2 normalizedPosition) { - this.normalizedArea.getPosition().setXY(normalizedPosition); - switch (this.horizontalAlignment) { - case LEFT -> this.area.getPosition().setX(normalizedPosition.getX()); - case CENTER -> this.area.getPosition().setX(normalizedPosition.getX() + (this.rendererControl.getEffectiveViewportDimension().getX() - RendererControl.REFERENCE_RESOLUTION.getX()) / 2); - case RIGHT -> this.area.getPosition().setX(normalizedPosition.getX() + this.rendererControl.getEffectiveViewportDimension().getX() - RendererControl.REFERENCE_RESOLUTION.getX()); - } - switch (this.verticalAlignment) { - case TOP -> this.area.getPosition().setY(normalizedPosition.getY()); - case CENTER -> this.area.getPosition().setY(normalizedPosition.getY() + (this.rendererControl.getEffectiveViewportDimension().getY() - RendererControl.REFERENCE_RESOLUTION.getY()) / 2); - case BOTTOM -> this.area.getPosition().setY(normalizedPosition.getY() + this.rendererControl.getEffectiveViewportDimension().getY() - RendererControl.REFERENCE_RESOLUTION.getY()); - } - this.draw(); - } - - /** - * Normalized position of this container relative to reference resolution, the actual position is automatically aligned based on alignment settings (e.g. if the user uses ultra-wide monitors). Global scale factors are not considered/handled by the renderer control. - */ - public CoordI2 getNormalizedPosition() { - return this.normalizedArea.getPosition().toImmutableCoordI2(); - } - - public void setNormalizedDimension(final CoordI2 normalizedDimension) { - this.normalizedArea.getDimension().setXY(normalizedDimension); - this.area.getDimension().setXY(normalizedDimension); - this.draw(); - } - - private Container getContainerUnderCursor(final MouseEvent event) { - Container containerUnderCursor = null; - synchronized (DISPLAYED_CONTAINERS) { - short zIndexHighest = -1; - for (final var container : DISPLAYED_CONTAINERS) { - if (container.dragging) { - containerUnderCursor = container; - break; - } - if (SpatialUtils2.intersects(container.area.getPosition(), container.area.getDimension(), event.getScaledCoord(), ImmutableCoordI2.one())) { - if (container.zIndex > zIndexHighest) { - zIndexHighest = container.zIndex; - containerUnderCursor = container; - } - } - } - } - return containerUnderCursor; - } - - private void zIndexToTop() { - if (!this.draggable) { - return; - } - - synchronized (DISPLAYED_CONTAINERS) { - final List path = this.getPathToRoot(this); - final Container root = path.get(0); - final Set pathSet = new HashSet<>(path); - - final Set displayed = new HashSet<>(DISPLAYED_CONTAINERS); - - final List others = new ArrayList<>(); - for (final Container c : DISPLAYED_CONTAINERS) { - if (!isDescendantOrSelf(root, c)) { - others.add(c); - } - } - others.sort((c1, c2) -> Short.compare(c1.getzIndex(), c2.getzIndex())); - - final Map subtreeMinDisplayedZIndex = new IdentityHashMap<>(); - final Map subtreeHasDisplayed = new IdentityHashMap<>(); - this.computeSubtreeInfo(root, displayed, subtreeMinDisplayedZIndex, subtreeHasDisplayed, new HashSet<>()); - - final List hierarchyDisplayed = new ArrayList<>(); - if (Boolean.TRUE.equals(subtreeHasDisplayed.get(root))) { - this.flattenDisplayedSubtree(root, displayed, pathSet, subtreeMinDisplayedZIndex, subtreeHasDisplayed, hierarchyDisplayed, new HashSet<>()); - } - - short z = 0; - for (final Container c : others) { - if (c.getzIndex() != z) { - c.setzIndex(z); - c.draw(); - } - z++; - } - for (final Container c : hierarchyDisplayed) { - if (c.getzIndex() != z) { - c.setzIndex(z); - c.draw(); - } - z++; - } - } - } - - private List getPathToRoot(final Container container) { - final List path = new ArrayList<>(); - Container current = container; - while (current != null) { - path.add(0, current); - current = current.parent; - } - return path; - } - - private boolean isDescendantOrSelf(final Container root, final Container c) { - Container curr = c; - while (curr != null) { - if (curr == root) { - return true; - } - curr = curr.parent; - } - return false; - } - - private void computeSubtreeInfo(final Container node, final Set displayedNodes, final Map subtreeMinDisplayedZIndex, final Map subtreeHasDisplayed, final Set visited) { - if (!visited.add(node)) { - return; - } - - short minZ = displayedNodes.contains(node) ? node.getzIndex() : Short.MAX_VALUE; - boolean hasDisplayed = displayedNodes.contains(node); - - for (final Container child : node.children) { - this.computeSubtreeInfo(child, displayedNodes, subtreeMinDisplayedZIndex, subtreeHasDisplayed, visited); - if (Boolean.TRUE.equals(subtreeHasDisplayed.get(child))) { - hasDisplayed = true; - final Short childMin = subtreeMinDisplayedZIndex.get(child); - if (childMin != null && childMin < minZ) { - minZ = childMin; - } - } - } - - subtreeHasDisplayed.put(node, hasDisplayed); - if (hasDisplayed) { - subtreeMinDisplayedZIndex.put(node, minZ); - } - } - - private void flattenDisplayedSubtree(final Container node, final Set displayedNodes, final Set pathSet, final Map subtreeMinDisplayedZIndex, final Map subtreeHasDisplayed, final List result, final Set visited) { - if (!visited.add(node)) { - return; - } - if (!Boolean.TRUE.equals(subtreeHasDisplayed.get(node))) { - return; - } - - if (displayedNodes.contains(node)) { - result.add(node); - } - - final List childrenSorted = new ArrayList<>(node.children); - childrenSorted.sort((c1, c2) -> { - final boolean c1OnPath = pathSet.contains(c1); - final boolean c2OnPath = pathSet.contains(c2); - if (c1OnPath != c2OnPath) { - return c1OnPath ? 1 : -1; - } - final short c1Key = subtreeMinDisplayedZIndex.getOrDefault(c1, Short.MAX_VALUE); - final short c2Key = subtreeMinDisplayedZIndex.getOrDefault(c2, Short.MAX_VALUE); - final int cmp = Short.compare(c1Key, c2Key); - if (cmp != 0) { - return cmp; - } - return Integer.compare(System.identityHashCode(c1), System.identityHashCode(c2)); - }); - - for (final Container child : childrenSorted) { - if (Boolean.TRUE.equals(subtreeHasDisplayed.get(child))) { - this.flattenDisplayedSubtree(child, displayedNodes, pathSet, subtreeMinDisplayedZIndex, subtreeHasDisplayed, result, visited); - } - } - } - - public void setParent(final Container parent) { - synchronized (DISPLAYED_CONTAINERS) { - if (this.parent != null) { - this.parent.children.remove(this); - } - this.parent = parent; - if (parent != null) { - parent.children.add(this); - } - } - } - - /** - * Normalized position of this container relative to reference resolution. Global scale factors are not considered/handled by the renderer control. - */ - public CoordI2 getNormalizedDimension() { - return this.normalizedArea.getDimension().toImmutableCoordI2(); - } - - public boolean isDraggable() { - return draggable; - } - - public void setDraggable(final boolean draggable) { - this.draggable = draggable; - } - - public HorizontalAlignment getHorizontalAlignment() { - return horizontalAlignment; - } - - public void setHorizontalAlignment(final HorizontalAlignment horizontalAlignment) { - this.horizontalAlignment = horizontalAlignment; - } - - public VerticalAlignment getVerticalAlignment() { - return verticalAlignment; - } - - public void setVerticalAlignment(final VerticalAlignment verticalAlignment) { - this.verticalAlignment = verticalAlignment; - } - - public short getzIndex() { - return zIndex; - } - - public void setzIndex(final short zIndex) { - this.zIndex = zIndex; - } - -} diff --git a/src/main/java/de/extio/game_engine/renderer/container/Window.java b/src/main/java/de/extio/game_engine/renderer/container/Window.java index df61714..b85d181 100644 --- a/src/main/java/de/extio/game_engine/renderer/container/Window.java +++ b/src/main/java/de/extio/game_engine/renderer/container/Window.java @@ -1,43 +1,232 @@ package de.extio.game_engine.renderer.container; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import de.extio.game_engine.event.EventService; +import de.extio.game_engine.module.AbstractClientModule; import de.extio.game_engine.module.ModuleService; import de.extio.game_engine.renderer.RendererControl; +import de.extio.game_engine.renderer.model.RenderingBo; import de.extio.game_engine.renderer.model.RenderingBoLayer; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.WindowCloseButtonControl; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.WindowPanelControl; +import de.extio.game_engine.renderer.model.bo.HorizontalAlignment; +import de.extio.game_engine.renderer.model.bo.VerticalAlignment; +import de.extio.game_engine.renderer.model.event.MouseClickEvent; +import de.extio.game_engine.renderer.model.event.MouseEvent; +import de.extio.game_engine.renderer.model.event.MouseMoveEvent; import de.extio.game_engine.renderer.model.event.UiControlEvent; +import de.extio.game_engine.renderer.model.event.ViewportResizeEvent; import de.extio.game_engine.renderer.work.RendererWorkingSet; import de.extio.game_engine.renderer.work.RenderingBoPool; +import de.extio.game_engine.spatial2.SpatialUtils2; +import de.extio.game_engine.spatial2.model.Area2; +import de.extio.game_engine.spatial2.model.CoordI2; +import de.extio.game_engine.spatial2.model.ImmutableCoordI2; +import de.extio.game_engine.spatial2.model.MutableCoordI2; @Component @Scope("prototype") -public class Window extends Container { +public class Window extends AbstractClientModule implements InitializingBean { - private boolean closeButton; + protected final static List DISPLAYED_CONTAINERS = Collections.synchronizedList(new ArrayList<>()); - private String closeButtonName = ""; + protected final ModuleService moduleService; - private Runnable onCloseAction; + protected final RenderingBoPool renderingBoPool; + + protected final RendererControl rendererControl; + + protected final RendererWorkingSet rendererWorkingSet; + + protected final EventService eventService; + + protected final Area2 area = new Area2(MutableCoordI2.create(), MutableCoordI2.create()); + + // Normalized area of this container relative to reference resolution, the actual position is automatically aligned based on alignment settings (e.g. if the user uses ultra-wide monitors). Global scale factors are not considered/handled by the renderer control. + protected final Area2 normalizedArea = new Area2(MutableCoordI2.create(), MutableCoordI2.create()); + + protected HorizontalAlignment horizontalAlignment = HorizontalAlignment.CENTER; + + protected VerticalAlignment verticalAlignment = VerticalAlignment.CENTER; + + protected short zIndex; + + protected Window parent; + + protected Set children = new HashSet<>(); + + protected Set components = new HashSet<>(); + + protected boolean draggable; + + protected boolean dragging; + + protected CoordI2 dragPrevPosition; + + protected boolean closeButton; + + protected String closeButtonName = ""; + + protected Runnable onCloseAction; public Window(final ModuleService moduleService, final RenderingBoPool renderingBoPool, final RendererControl rendererControl, final EventService eventService, final RendererWorkingSet rendererWorkingSet) { - super(moduleService, renderingBoPool, rendererControl, eventService, rendererWorkingSet); + this.moduleService = moduleService; + this.renderingBoPool = renderingBoPool; + this.rendererControl = rendererControl; + this.eventService = eventService; + this.rendererWorkingSet = rendererWorkingSet; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.moduleService.loadModule(this); } @Override public void onActivate() { + this.eventService.register(ViewportResizeEvent.class, this.id, this::onViewportResize); + this.eventService.register(MouseMoveEvent.class, this.id, this::onMouseMoveEvent); + this.eventService.register(MouseClickEvent.class, this.id, this::onMouseClickEvent); if (this.closeButton) { this.eventService.register(UiControlEvent.class, this.getId(), this::onCloseButtonControlEvent); } - - super.onActivate(); + this.onViewportResize(null); } @Override + public void onShow() { + DISPLAYED_CONTAINERS.add(this); + this.zIndexToTop(); + this.components.forEach(c -> c.onAddedToWindow()); + this.draw(); + } + + @Override + public void onHide() { + DISPLAYED_CONTAINERS.remove(this); + this.components.forEach(WindowComponent::onRemovedFromWindow); + } + + protected void onViewportResize(final ViewportResizeEvent event) { + this.setNormalizedPosition(this.normalizedArea.getPosition()); + this.setNormalizedDimension(this.normalizedArea.getDimension()); + this.adjustIfOutsideViewport(); + this.draw(); + } + + private boolean adjustIfOutsideViewport() { + final var viewportDimension = this.rendererControl.getEffectiveViewportDimension(); + final var position = this.area.getPosition(); + final var dimension = this.area.getDimension(); + + final var containerArea = this.area.getDimension().getX() * this.area.getDimension().getY(); + final var intersectionArea = this.calculateIntersectionArea(position, dimension, viewportDimension); + final var intersectionPercentage = containerArea > 0 ? (double) intersectionArea / containerArea : 0.0; + + if (intersectionPercentage >= 0.1) { + return false; + } + + int adjustedX = position.getX(); + int adjustedY = position.getY(); + + if (position.getX() >= viewportDimension.getX()) { + adjustedX = viewportDimension.getX() - dimension.getX(); + } + else if (position.getX() + dimension.getX() <= 0) { + adjustedX = 0; + } + else if (position.getX() < 0) { + adjustedX = 0; + } + else if (position.getX() + dimension.getX() > viewportDimension.getX()) { + adjustedX = viewportDimension.getX() - dimension.getX(); + } + + if (position.getY() >= viewportDimension.getY()) { + adjustedY = viewportDimension.getY() - dimension.getY(); + } + else if (position.getY() + dimension.getY() <= 0) { + adjustedY = 0; + } + else if (position.getY() < 0) { + adjustedY = 0; + } + else if (position.getY() + dimension.getY() > viewportDimension.getY()) { + adjustedY = viewportDimension.getY() - dimension.getY(); + } + + this.normalizedArea.getPosition().setXY(adjustedX, adjustedY); + this.area.getPosition().setXY(adjustedX, adjustedY); + + this.LOGGER.debug("Adjusted container {} position to {},{} to remain visible in viewport", this.id, adjustedX, adjustedY); + return true; + } + + private int calculateIntersectionArea(final CoordI2 position, final CoordI2 dimension, final CoordI2 viewportDimension) { + final int x1 = Math.max(0, position.getX()); + final int y1 = Math.max(0, position.getY()); + final int x2 = Math.min(viewportDimension.getX(), position.getX() + dimension.getX()); + final int y2 = Math.min(viewportDimension.getY(), position.getY() + dimension.getY()); + + final int width = Math.max(0, x2 - x1); + final int height = Math.max(0, y2 - y1); + + return width * height; + } + + protected void onMouseMoveEvent(final MouseMoveEvent event) { + if (this.draggable && event.isDrag()) { + final Window containerUnderCursor = getContainerUnderCursor(event); + if (containerUnderCursor == this) { + if (!this.dragging) { + this.dragging = true; + this.dragPrevPosition = null; + this.zIndexToTop(); + this.LOGGER.debug("Started dragging container {}", this.id); + } + if (this.dragPrevPosition != null) { + final var delta = event.getScaledCoord().substract(this.dragPrevPosition); + if (!ImmutableCoordI2.zero().equals(delta)) { + this.setNormalizedPosition(this.getNormalizedPosition().add(delta)); + } + } + this.dragPrevPosition = event.getScaledCoord(); + } + } + } + + protected void onMouseClickEvent(final MouseClickEvent event) { + final Window containerUnderCursor = this.getContainerUnderCursor(event); + if (containerUnderCursor == this) { + if (event.isPressed() && !this.dragging) { + this.zIndexToTop(); + } + } + + if (!event.isPressed() && this.dragging) { + this.dragging = false; + this.dragPrevPosition = null; + if (this.adjustIfOutsideViewport()) { + this.draw(); + } + this.LOGGER.debug("Stopped dragging container {}", this.id); + } + } + public void draw() { if (this.area.getDimension().getX() <= 0 || this.area.getDimension().getY() <= 0) { return; @@ -74,10 +263,266 @@ public class Window extends Container { this.rendererWorkingSet.put(this.id, closeButtonBo); } - super.draw(); + for (final var component : this.components) { + component.draw(); + } + + final var uncommitted = this.rendererWorkingSet.getUncommittedWork(this.id); + for (final RenderingBo bo : uncommitted.values()) { + bo.setZIndex(this.zIndex); + + CoordI2 componentOffset = ImmutableCoordI2.zero(); + CoordI2 componentExtraOffset = ImmutableCoordI2.zero(); + for (final var component : this.components) { + if (component.getRenderingBoIds().contains(bo.getId())) { + componentOffset = component.getRelativePosition(); + componentExtraOffset = component.getRenderingBoExtraOffset(bo); + break; + } + } + bo.withPositionAbsoluteAnchorTopLeft( + this.area.getPosition() + .toMutableCoordI2() + .add(bo.getLocalX(), bo.getLocalY()) + .add(componentOffset) + .add(componentExtraOffset)); + + this.rendererWorkingSet.put(this.id, bo); + } + + this.rendererWorkingSet.commit(this.id, true); } - private void onCloseButtonControlEvent(final UiControlEvent event) { + /** + * Returns an unmodifiable DEEP copy of rendering BOs belonging to this container. + * If a RenderingBo is modified after this call, the changes will NOT be reflected in the active working set. You must call this.{@link #putRenderingBo(RenderingBo)} to update the working set. + */ + public Map getRenderingBos() { + return this.rendererWorkingSet.getUncommittedWork(this.id); + } + + public void putRenderingBo(final RenderingBo renderingBo) { + this.rendererWorkingSet.put(this.id, renderingBo); + } + + public void removeRenderingBo(final String renderingBoId) { + this.rendererWorkingSet.remove(this.id, renderingBoId); + } + + public void clearRenderingBos() { + this.rendererWorkingSet.clearNext(this.id); + } + + public void setNormalizedPosition(final CoordI2 normalizedPosition) { + this.normalizedArea.getPosition().setXY(normalizedPosition); + switch (this.horizontalAlignment) { + case LEFT -> this.area.getPosition().setX(normalizedPosition.getX()); + case CENTER -> this.area.getPosition().setX(normalizedPosition.getX() + (this.rendererControl.getEffectiveViewportDimension().getX() - RendererControl.REFERENCE_RESOLUTION.getX()) / 2); + case RIGHT -> this.area.getPosition().setX(normalizedPosition.getX() + this.rendererControl.getEffectiveViewportDimension().getX() - RendererControl.REFERENCE_RESOLUTION.getX()); + } + switch (this.verticalAlignment) { + case TOP -> this.area.getPosition().setY(normalizedPosition.getY()); + case CENTER -> this.area.getPosition().setY(normalizedPosition.getY() + (this.rendererControl.getEffectiveViewportDimension().getY() - RendererControl.REFERENCE_RESOLUTION.getY()) / 2); + case BOTTOM -> this.area.getPosition().setY(normalizedPosition.getY() + this.rendererControl.getEffectiveViewportDimension().getY() - RendererControl.REFERENCE_RESOLUTION.getY()); + } + this.draw(); + } + + /** + * Normalized position of this container relative to reference resolution, the actual position is automatically aligned based on alignment settings (e.g. if the user uses ultra-wide monitors). Global scale factors are not considered/handled by the renderer control. + */ + public CoordI2 getNormalizedPosition() { + return this.normalizedArea.getPosition().toImmutableCoordI2(); + } + + public void setNormalizedDimension(final CoordI2 normalizedDimension) { + this.normalizedArea.getDimension().setXY(normalizedDimension); + this.area.getDimension().setXY(normalizedDimension); + this.draw(); + } + + /** + * Normalized position of this container relative to reference resolution. Global scale factors are not considered/handled by the renderer control. + */ + public CoordI2 getNormalizedDimension() { + return this.normalizedArea.getDimension().toImmutableCoordI2(); + } + + private Window getContainerUnderCursor(final MouseEvent event) { + Window containerUnderCursor = null; + synchronized (DISPLAYED_CONTAINERS) { + short zIndexHighest = -1; + for (final var container : DISPLAYED_CONTAINERS) { + if (container.dragging) { + containerUnderCursor = container; + break; + } + if (SpatialUtils2.intersects(container.area.getPosition(), container.area.getDimension(), event.getScaledCoord(), ImmutableCoordI2.one())) { + if (container.zIndex > zIndexHighest) { + zIndexHighest = container.zIndex; + containerUnderCursor = container; + } + } + } + } + return containerUnderCursor; + } + + private void zIndexToTop() { + if (!this.draggable) { + return; + } + + synchronized (DISPLAYED_CONTAINERS) { + final List path = this.getPathToRoot(this); + final Window root = path.get(0); + final Set pathSet = new HashSet<>(path); + + final Set displayed = new HashSet<>(DISPLAYED_CONTAINERS); + + final List others = new ArrayList<>(); + for (final Window c : DISPLAYED_CONTAINERS) { + if (!isDescendantOrSelf(root, c)) { + others.add(c); + } + } + others.sort((c1, c2) -> Short.compare(c1.getzIndex(), c2.getzIndex())); + + final Map subtreeMinDisplayedZIndex = new IdentityHashMap<>(); + final Map subtreeHasDisplayed = new IdentityHashMap<>(); + this.computeSubtreeInfo(root, displayed, subtreeMinDisplayedZIndex, subtreeHasDisplayed, new HashSet<>()); + + final List hierarchyDisplayed = new ArrayList<>(); + if (Boolean.TRUE.equals(subtreeHasDisplayed.get(root))) { + this.flattenDisplayedSubtree(root, displayed, pathSet, subtreeMinDisplayedZIndex, subtreeHasDisplayed, hierarchyDisplayed, new HashSet<>()); + } + + short z = 0; + for (final Window c : others) { + if (c.getzIndex() != z) { + c.setzIndex(z); + c.draw(); + } + z++; + } + for (final Window c : hierarchyDisplayed) { + if (c.getzIndex() != z) { + c.setzIndex(z); + c.draw(); + } + z++; + } + } + } + + private List getPathToRoot(final Window container) { + final List path = new ArrayList<>(); + Window current = container; + while (current != null) { + path.add(0, current); + current = current.parent; + } + return path; + } + + private boolean isDescendantOrSelf(final Window root, final Window c) { + Window curr = c; + while (curr != null) { + if (curr == root) { + return true; + } + curr = curr.parent; + } + return false; + } + + private void computeSubtreeInfo(final Window node, final Set displayedNodes, final Map subtreeMinDisplayedZIndex, final Map subtreeHasDisplayed, final Set visited) { + if (!visited.add(node)) { + return; + } + + short minZ = displayedNodes.contains(node) ? node.getzIndex() : Short.MAX_VALUE; + boolean hasDisplayed = displayedNodes.contains(node); + + for (final Window child : node.children) { + this.computeSubtreeInfo(child, displayedNodes, subtreeMinDisplayedZIndex, subtreeHasDisplayed, visited); + if (Boolean.TRUE.equals(subtreeHasDisplayed.get(child))) { + hasDisplayed = true; + final Short childMin = subtreeMinDisplayedZIndex.get(child); + if (childMin != null && childMin < minZ) { + minZ = childMin; + } + } + } + + subtreeHasDisplayed.put(node, hasDisplayed); + if (hasDisplayed) { + subtreeMinDisplayedZIndex.put(node, minZ); + } + } + + private void flattenDisplayedSubtree(final Window node, final Set displayedNodes, final Set pathSet, final Map subtreeMinDisplayedZIndex, final Map subtreeHasDisplayed, final List result, final Set visited) { + if (!visited.add(node)) { + return; + } + if (!Boolean.TRUE.equals(subtreeHasDisplayed.get(node))) { + return; + } + + if (displayedNodes.contains(node)) { + result.add(node); + } + + final List childrenSorted = new ArrayList<>(node.children); + childrenSorted.sort((c1, c2) -> { + final boolean c1OnPath = pathSet.contains(c1); + final boolean c2OnPath = pathSet.contains(c2); + if (c1OnPath != c2OnPath) { + return c1OnPath ? 1 : -1; + } + final short c1Key = subtreeMinDisplayedZIndex.getOrDefault(c1, Short.MAX_VALUE); + final short c2Key = subtreeMinDisplayedZIndex.getOrDefault(c2, Short.MAX_VALUE); + final int cmp = Short.compare(c1Key, c2Key); + if (cmp != 0) { + return cmp; + } + return Integer.compare(System.identityHashCode(c1), System.identityHashCode(c2)); + }); + + for (final Window child : childrenSorted) { + if (Boolean.TRUE.equals(subtreeHasDisplayed.get(child))) { + this.flattenDisplayedSubtree(child, displayedNodes, pathSet, subtreeMinDisplayedZIndex, subtreeHasDisplayed, result, visited); + } + } + } + + public void setParent(final Window parent) { + synchronized (DISPLAYED_CONTAINERS) { + if (this.parent != null) { + this.parent.children.remove(this); + } + this.parent = parent; + if (parent != null) { + parent.children.add(this); + } + } + } + + public void addComponent(final WindowComponent component) { + this.components.add(Objects.requireNonNull(component)); + component.setParent(this); + if (this.isDisplayed()) { + component.onAddedToWindow(); + } + } + + public void removeComponent(final WindowComponent component) { + if (this.components.remove(component) && this.isDisplayed()) { + component.onRemovedFromWindow(); + } + } + + protected void onCloseButtonControlEvent(final UiControlEvent event) { if (this.closeButton && this.closeButtonName.equals(event.getId())) { if (this.onCloseAction != null) { this.onCloseAction.run(); @@ -88,6 +533,38 @@ public class Window extends Container { } } + public boolean isDraggable() { + return draggable; + } + + public void setDraggable(final boolean draggable) { + this.draggable = draggable; + } + + public HorizontalAlignment getHorizontalAlignment() { + return horizontalAlignment; + } + + public void setHorizontalAlignment(final HorizontalAlignment horizontalAlignment) { + this.horizontalAlignment = horizontalAlignment; + } + + public VerticalAlignment getVerticalAlignment() { + return verticalAlignment; + } + + public void setVerticalAlignment(final VerticalAlignment verticalAlignment) { + this.verticalAlignment = verticalAlignment; + } + + public short getzIndex() { + return zIndex; + } + + public void setzIndex(final short zIndex) { + this.zIndex = zIndex; + } + public boolean hasCloseButton() { return closeButton; } @@ -112,4 +589,9 @@ public class Window extends Container { public void setOnCloseAction(final Runnable onCloseAction) { this.onCloseAction = onCloseAction; } + + public CoordI2 getAbsolutePosition() { + return this.area.getPosition().toImmutableCoordI2(); + } + } diff --git a/src/main/java/de/extio/game_engine/renderer/container/WindowComponent.java b/src/main/java/de/extio/game_engine/renderer/container/WindowComponent.java new file mode 100644 index 0000000..7e9dd7a --- /dev/null +++ b/src/main/java/de/extio/game_engine/renderer/container/WindowComponent.java @@ -0,0 +1,24 @@ +package de.extio.game_engine.renderer.container; + +import java.util.Set; + +import de.extio.game_engine.renderer.model.RenderingBo; +import de.extio.game_engine.spatial2.model.CoordI2; + +public interface WindowComponent { + + void setParent(Window window); + + void onAddedToWindow(); + + void onRemovedFromWindow(); + + Set getRenderingBoIds(); + + CoordI2 getRelativePosition(); + + CoordI2 getRenderingBoExtraOffset(RenderingBo renderingBo); + + void draw(); + +} diff --git a/src/main/java/de/extio/game_engine/renderer/container/legacy/ScrollBar.java b/src/main/java/de/extio/game_engine/renderer/container/legacy/ScrollBar.java deleted file mode 100644 index 1ed6d88..0000000 --- a/src/main/java/de/extio/game_engine/renderer/container/legacy/ScrollBar.java +++ /dev/null @@ -1,94 +0,0 @@ -package de.extio.game_engine.renderer.container.legacy; - -import java.util.List; -import java.util.function.Predicate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import de.extio.game_engine.renderer.model.RenderingBo; -import de.extio.game_engine.renderer.model.RenderingBoLayer; -import de.extio.game_engine.renderer.model.bo.ControlRenderingBo; -import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderControl; -import de.extio.game_engine.renderer.model.color.RgbaColor; -import de.extio.game_engine.renderer.model.event.UiControlEvent; -import de.extio.game_engine.renderer.work.RenderingBoPool; -import de.extio.game_engine.spatial2.model.Area2; -import de.extio.game_engine.spatial2.model.CoordI2; - -/** - * Helper for scrollbars. - * I recommend to check for code examples to learn more about the usage of this helper and how to integrate in client modules. - */ -public class ScrollBar { - - private static final Logger LOGGER = LoggerFactory.getLogger(ScrollBar.class); - - private final String name; - - private double scrollPosition; - - private int lastNumElements; - - private short layer = RenderingBoLayer.UI1; - - public ScrollBar(final String nameSuffix) { - this.name = "Scrollbar_" + nameSuffix; - } - - public void onEvent(final UiControlEvent event, final Predicate positionIntersectsPredicate) { - final var uiControlEvent = (UiControlEvent) event; - - if (this.name.equals(uiControlEvent.getId())) { - this.scrollPosition = 1.0D - ((Double) uiControlEvent.getPayload()).doubleValue(); - } - } - - public void onMouseScroll(final int button, final CoordI2 absolutePos, final Predicate positionIntersectsPredicate) { - if ((button == 4 || button == 5) && - this.lastNumElements > 0 && - positionIntersectsPredicate.test(absolutePos)) { - - final var perElem = (button == 4 ? -1.0 : 1.0) / this.lastNumElements; - this.scrollPosition = Math.min(1.0, Math.max(0.0, this.scrollPosition + perElem)); - } - } - - public void render(final List renderingBo, final Area2 renderingArea, final int numElements, final RenderingBoPool renderingBoPool) { - this.lastNumElements = numElements; - - final var bo = renderingBoPool.acquire(this.name + "_Slider", ControlRenderingBo.class) - .setType(SliderControl.class) - .setCustomData(Boolean.FALSE) - .setCustomData2(Double.valueOf(1.0D - this.scrollPosition)) - .setCustomData3(Double.valueOf(1.0D - this.scrollPosition)) - .setEnabled(numElements > 0) - .setVisible(true) - .withDimensionAbsolute(20, renderingArea.getDimension().getY()) - .withPositionAbsoluteAnchorTopLeft(renderingArea.getPosition().getX() + renderingArea.getDimension().getX() - 20, renderingArea.getPosition().getY()) - .setColor(RgbaColor.LIGHT_GRAY) - .setLayer(this.layer); - renderingBo.add(bo); - } - - public double getScrollPosition() { - return this.scrollPosition; - } - - public void setScrollPosition(final double scrollPosition) { - this.scrollPosition = scrollPosition; - } - - public String getName() { - return this.name; - } - - public short getLayer() { - return this.layer; - } - - public void setLayer(final short layer) { - this.layer = layer; - } - -}