diff --git a/src/main/java/com/cleanroommc/modularui/ClientProxy.java b/src/main/java/com/cleanroommc/modularui/ClientProxy.java index 0510e3f9b..93a2579b6 100644 --- a/src/main/java/com/cleanroommc/modularui/ClientProxy.java +++ b/src/main/java/com/cleanroommc/modularui/ClientProxy.java @@ -10,7 +10,6 @@ import com.cleanroommc.modularui.keybind.KeyBindHandler; import com.cleanroommc.modularui.screen.ClientScreenHandler; import com.cleanroommc.modularui.test.EventHandler; -import com.cleanroommc.modularui.test.OverlayTest; import com.cleanroommc.modularui.test.TestItem; import com.cleanroommc.modularui.theme.ThemeManager; import com.cleanroommc.modularui.theme.ThemeReloadCommand; @@ -70,9 +69,6 @@ void preInit(FMLPreInitializationEvent event) { testKey = new KeyBinding("key.test", KeyConflictContext.IN_GAME, Keyboard.KEY_NUMPAD4, "key.categories.modularui"); ClientRegistry.registerKeyBinding(testKey); } - if (ModularUIConfig.enableTestOverlays) { - OverlayTest.init(); - } DrawableSerialization.init(); RenderingRegistry.registerEntityRenderingHandler(HoloScreenEntity.class, ScreenEntityRender::new); diff --git a/src/main/java/com/cleanroommc/modularui/GuiError.java b/src/main/java/com/cleanroommc/modularui/GuiError.java index fcf596241..077f3de6b 100644 --- a/src/main/java/com/cleanroommc/modularui/GuiError.java +++ b/src/main/java/com/cleanroommc/modularui/GuiError.java @@ -1,9 +1,6 @@ package com.cleanroommc.modularui; -import com.cleanroommc.modularui.api.widget.IGuiElement; - -import com.cleanroommc.modularui.network.NetworkHandler; - +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.network.NetworkUtils; import org.apache.logging.log4j.Level; @@ -12,7 +9,7 @@ public class GuiError { - public static void throwNew(IGuiElement guiElement, Type type, String msg) { + public static void throwNew(IWidget guiElement, Type type, String msg) { if (NetworkUtils.isClient()) { GuiErrorHandler.INSTANCE.pushError(guiElement, type, msg); } @@ -20,10 +17,10 @@ public static void throwNew(IGuiElement guiElement, Type type, String msg) { private final Level level = Level.ERROR; private final String msg; - private final IGuiElement reference; + private final IWidget reference; private final Type type; - protected GuiError(String msg, IGuiElement reference, Type type) { + protected GuiError(String msg, IWidget reference, Type type) { this.msg = msg; this.reference = reference; this.type = type; @@ -33,7 +30,7 @@ public Level getLevel() { return level; } - public IGuiElement getReference() { + public IWidget getReference() { return reference; } diff --git a/src/main/java/com/cleanroommc/modularui/GuiErrorHandler.java b/src/main/java/com/cleanroommc/modularui/GuiErrorHandler.java index 2251e265c..4030ad77e 100644 --- a/src/main/java/com/cleanroommc/modularui/GuiErrorHandler.java +++ b/src/main/java/com/cleanroommc/modularui/GuiErrorHandler.java @@ -1,6 +1,6 @@ package com.cleanroommc.modularui; -import com.cleanroommc.modularui.api.widget.IGuiElement; +import com.cleanroommc.modularui.api.widget.IWidget; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; @@ -25,7 +25,7 @@ public void clear() { this.errors.clear(); } - void pushError(IGuiElement reference, GuiError.Type type, String msg) { + void pushError(IWidget reference, GuiError.Type type, String msg) { GuiError error = new GuiError(msg, reference, type); if (this.errorSet.add(error)) { ModularUI.LOGGER.log(error.getLevel(), error); diff --git a/src/main/java/com/cleanroommc/modularui/ModularUI.java b/src/main/java/com/cleanroommc/modularui/ModularUI.java index 5bf867b34..2f9ec2b47 100644 --- a/src/main/java/com/cleanroommc/modularui/ModularUI.java +++ b/src/main/java/com/cleanroommc/modularui/ModularUI.java @@ -7,6 +7,7 @@ import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLServerStartingEvent; +import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -36,6 +37,8 @@ public class ModularUI { serverSide = "com.cleanroommc.modularui.CommonProxy") public static CommonProxy proxy; + public static final boolean isDev = FMLLaunchHandler.isDeobfuscatedEnvironment(); + @Mod.Instance public static ModularUI INSTANCE; diff --git a/src/main/java/com/cleanroommc/modularui/animation/Animator.java b/src/main/java/com/cleanroommc/modularui/animation/Animator.java index 17a844b61..fcca6ce1f 100644 --- a/src/main/java/com/cleanroommc/modularui/animation/Animator.java +++ b/src/main/java/com/cleanroommc/modularui/animation/Animator.java @@ -171,8 +171,6 @@ public Animator curve(IInterpolation curve) { return this; } - - /** * Sets a function which is executed everytime the progress updates, that is on every frame. * The argument of the function is the interpolated value. diff --git a/src/main/java/com/cleanroommc/modularui/api/IGuiHolder.java b/src/main/java/com/cleanroommc/modularui/api/IGuiHolder.java index b189e6ec4..79570413e 100644 --- a/src/main/java/com/cleanroommc/modularui/api/IGuiHolder.java +++ b/src/main/java/com/cleanroommc/modularui/api/IGuiHolder.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.api; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.factory.GuiData; import com.cleanroommc.modularui.screen.ModularPanel; import com.cleanroommc.modularui.screen.ModularScreen; @@ -24,7 +25,8 @@ public interface IGuiHolder { */ @SideOnly(Side.CLIENT) default ModularScreen createScreen(T data, ModularPanel mainPanel) { - return new ModularScreen(mainPanel); + ModularUI.LOGGER.warn("IGuiHolder.createScreen() should be overridden to pass you own mod id to the ModularScreen. In future versions this method must be overridden or else it will crash!"); + return new ModularScreen(ModularUI.ID, mainPanel); } /** diff --git a/src/main/java/com/cleanroommc/modularui/api/IThemeApi.java b/src/main/java/com/cleanroommc/modularui/api/IThemeApi.java index b9fa69ab4..d20af1e90 100644 --- a/src/main/java/com/cleanroommc/modularui/api/IThemeApi.java +++ b/src/main/java/com/cleanroommc/modularui/api/IThemeApi.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.drawable.GuiTextures; import com.cleanroommc.modularui.drawable.Scrollbar; +import com.cleanroommc.modularui.screen.ModularPanel; import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.theme.SelectableTheme; import com.cleanroommc.modularui.theme.SlotTheme; @@ -167,7 +168,32 @@ default void registerTheme(ThemeBuilder themeBuilder) { * @param defaultTheme default theme if no theme was found * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} */ - ITheme getThemeForScreen(String owner, String name, @Nullable String defaultTheme); + default ITheme getThemeForScreen(String owner, String name, @Nullable String defaultTheme, @Nullable String fallbackTheme) { + return getThemeForScreen(owner, name, null, defaultTheme, fallbackTheme); + } + + /** + * Gets the appropriate theme for a screen. + * + * @param owner owner of the screen + * @param name name of the screen + * @param panel the name + * @param defaultTheme default theme if no theme was found + * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} + */ + ITheme getThemeForScreen(String owner, String name, @Nullable String panel, @Nullable String defaultTheme, @Nullable String fallbackTheme); + + /** + * Gets the appropriate theme for a specific panel. + * + * @param panel the panel to find a theme for + * @param defaultTheme default theme if no theme was found + * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} + */ + default ITheme getThemeForScreen(ModularPanel panel, @Nullable String defaultTheme) { + ModularScreen screen = panel.getScreen(); + return getThemeForScreen(screen.getOwner(), screen.getName(), panel.getName(), defaultTheme, screen.getThemeOverride()); + } /** * Gets the appropriate theme for a screen. @@ -177,7 +203,7 @@ default void registerTheme(ThemeBuilder themeBuilder) { * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} */ default ITheme getThemeForScreen(ModularScreen screen, @Nullable String defaultTheme) { - return getThemeForScreen(screen.getOwner(), screen.getName(), defaultTheme); + return getThemeForScreen(screen.getOwner(), screen.getName(), defaultTheme, null); } /** diff --git a/src/main/java/com/cleanroommc/modularui/api/ITreeNode.java b/src/main/java/com/cleanroommc/modularui/api/ITreeNode.java new file mode 100644 index 000000000..d5718f87f --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/api/ITreeNode.java @@ -0,0 +1,18 @@ +package com.cleanroommc.modularui.api; + +import java.util.List; + +public interface ITreeNode> { + + T getParent(); + + default boolean hasParent() { + return getParent() != null; + } + + List getChildren(); + + default boolean hasChildren() { + return !getChildren().isEmpty(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/api/layout/IResizeParent.java b/src/main/java/com/cleanroommc/modularui/api/layout/IResizeParent.java new file mode 100644 index 000000000..b4ad84519 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/api/layout/IResizeParent.java @@ -0,0 +1,75 @@ +package com.cleanroommc.modularui.api.layout; + +import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.widget.sizer.Area; + +public interface IResizeParent { + + /** + * @return area of the element + */ + Area getArea(); + + /** + * @return true if the relative x position is calculated + */ + boolean isXCalculated(); + + /** + * @return true if the relative y position is calculated + */ + boolean isYCalculated(); + + /** + * @return true if the width is calculated + */ + boolean isWidthCalculated(); + + /** + * @return true if the height is calculated + */ + boolean isHeightCalculated(); + + boolean areChildrenCalculated(); + + boolean isLayoutDone(); + + boolean canRelayout(boolean isParentLayout); + + default boolean isSizeCalculated(GuiAxis axis) { + return axis.isHorizontal() ? isWidthCalculated() : isHeightCalculated(); + } + + default boolean isPosCalculated(GuiAxis axis) { + return axis.isHorizontal() ? isXCalculated() : isYCalculated(); + } + + /** + * @return true if the relative position and size are fully calculated + */ + default boolean isSelfFullyCalculated(boolean isParentLayout) { + return isSelfFullyCalculated() && !canRelayout(isParentLayout); + } + + default boolean isSelfFullyCalculated() { + return isXCalculated() && isYCalculated() && isWidthCalculated() && isHeightCalculated(); + } + + default boolean isFullyCalculated() { + return isSelfFullyCalculated() && areChildrenCalculated() && isLayoutDone(); + } + + default boolean isFullyCalculated(boolean isParentLayout) { + return isFullyCalculated() && !canRelayout(isParentLayout); + } + + /** + * @return true if margin and padding are applied on the x-axis + */ + boolean isXMarginPaddingApplied(); + + /** + * @return true if margin and padding are applied on the y-axis + */ + boolean isYMarginPaddingApplied(); +} diff --git a/src/main/java/com/cleanroommc/modularui/api/layout/IResizeable.java b/src/main/java/com/cleanroommc/modularui/api/layout/IResizeable.java index 715f8fdc4..ddc0d8312 100644 --- a/src/main/java/com/cleanroommc/modularui/api/layout/IResizeable.java +++ b/src/main/java/com/cleanroommc/modularui/api/layout/IResizeable.java @@ -1,103 +1,43 @@ package com.cleanroommc.modularui.api.layout; import com.cleanroommc.modularui.api.GuiAxis; -import com.cleanroommc.modularui.api.widget.IGuiElement; -import com.cleanroommc.modularui.widget.sizer.Area; /** * An interface that handles resizing of widgets. * Usually this interface is not implemented by the users of this library or will even interact with it. */ -public interface IResizeable { +public interface IResizeable extends IResizeParent { /** * Called once before resizing */ - void initResizing(); + void initResizing(boolean onOpen); /** * Resizes the given element * - * @param guiElement element to resize * @param isParentLayout if the parent is a layout widget * @return true if element is fully resized */ - boolean resize(IGuiElement guiElement, boolean isParentLayout); + boolean resize(boolean isParentLayout); /** - * Called if {@link #resize(IGuiElement, boolean)} returned false after children have been resized. + * Called if {@link #resize(boolean)} returned false after children have been resized. * - * @param guiElement element to resize * @return if element is fully resized */ - boolean postResize(IGuiElement guiElement); + boolean postResize(); /** * Called after all elements in the tree are resized and the absolute positions needs to be calculated from the * relative postion. - * - * @param guiElement element that was resized - */ - default void applyPos(IGuiElement guiElement) {} - - /** - * @return area of the element - */ - // TODO doesnt fit with the other api methods in this interface - Area getArea(); - - /** - * @return true if the relative x position is calculated - */ - boolean isXCalculated(); - - /** - * @return true if the relative y position is calculated */ - boolean isYCalculated(); - - /** - * @return true if the width is calculated - */ - boolean isWidthCalculated(); - - /** - * @return true if the height is calculated - */ - boolean isHeightCalculated(); - - boolean areChildrenCalculated(); - - boolean isLayoutDone(); - - default boolean isSizeCalculated(GuiAxis axis) { - return axis.isHorizontal() ? isWidthCalculated() : isHeightCalculated(); - } - - default boolean isPosCalculated(GuiAxis axis) { - return axis.isHorizontal() ? isXCalculated() : isYCalculated(); - } + default void preApplyPos() {} /** - * @return true if the relative position and size are fully calculated + * This converts the relative pos to resizer parent to relative pos to widget parent. */ - default boolean isSelfFullyCalculated(boolean isParentLayout) { - return isSelfFullyCalculated() && !canRelayout(isParentLayout); - } - - default boolean isSelfFullyCalculated() { - return isXCalculated() && isYCalculated() && isWidthCalculated() && isHeightCalculated(); - } - - default boolean isFullyCalculated() { - return isSelfFullyCalculated() && areChildrenCalculated() && isLayoutDone(); - } - - default boolean isFullyCalculated(boolean isParentLayout) { - return isSelfFullyCalculated(isParentLayout) && areChildrenCalculated() && isLayoutDone(); - } - - boolean canRelayout(boolean isParentLayout); + default void applyPos() {} void setChildrenResized(boolean resized); @@ -182,14 +122,4 @@ default void setMarginPaddingApplied(GuiAxis axis, boolean b) { setYMarginPaddingApplied(b); } } - - /** - * @return true if margin and padding are applied on the x-axis - */ - boolean isXMarginPaddingApplied(); - - /** - * @return true if margin and padding are applied on the y-axis - */ - boolean isYMarginPaddingApplied(); } diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IDelegatingWidget.java b/src/main/java/com/cleanroommc/modularui/api/widget/IDelegatingWidget.java new file mode 100644 index 000000000..b8132f336 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IDelegatingWidget.java @@ -0,0 +1,18 @@ +package com.cleanroommc.modularui.api.widget; + +import net.minecraft.inventory.Slot; + +public interface IDelegatingWidget extends IWidget, IVanillaSlot { + + IWidget getDelegate(); + + @Override + default Slot getVanillaSlot() { + return getDelegate() instanceof IVanillaSlot vanillaSlot ? vanillaSlot.getVanillaSlot() : null; + } + + @Override + default boolean handleAsVanillaSlot() { + return getDelegate() instanceof IVanillaSlot vanillaSlot && vanillaSlot.handleAsVanillaSlot(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IDragResizeable.java b/src/main/java/com/cleanroommc/modularui/api/widget/IDragResizeable.java index 4ac2ee934..4a03cf0f6 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IDragResizeable.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IDragResizeable.java @@ -88,14 +88,14 @@ static void applyDrag(IDragResizeable resizeable, IWidget widget, ResizeDragArea if (dragArea.left) { int s = startArea.width - dx * keepPosFactor; if (s >= resizeable.getMinDragWidth()) { - widget.flex().left(startArea.rx + dx); - widget.flex().width(s); + widget.resizer().left(startArea.rx + dx); + widget.resizer().width(s); } } else if (dragArea.right) { int s = startArea.width + dx * keepPosFactor; if (s >= resizeable.getMinDragWidth()) { - widget.flex().left(startArea.rx - dx * (keepPosFactor - 1)); - widget.flex().width(s); + widget.resizer().left(startArea.rx - dx * (keepPosFactor - 1)); + widget.resizer().width(s); } } } @@ -103,14 +103,14 @@ static void applyDrag(IDragResizeable resizeable, IWidget widget, ResizeDragArea if (dragArea.top) { int s = startArea.height - dy * keepPosFactor; if (s >= resizeable.getMinDragHeight()) { - widget.flex().top(startArea.ry + dy); - widget.flex().height(s); + widget.resizer().top(startArea.ry + dy); + widget.resizer().height(s); } } else if (dragArea.bottom) { int s = startArea.height + dy * keepPosFactor; if (s >= resizeable.getMinDragHeight()) { - widget.flex().top(startArea.ry - dy * (keepPosFactor - 1)); - widget.flex().height(s); + widget.resizer().top(startArea.ry - dy * (keepPosFactor - 1)); + widget.resizer().height(s); } } } diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IDraggable.java b/src/main/java/com/cleanroommc/modularui/api/widget/IDraggable.java index f70bd174b..bb240f331 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IDraggable.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IDraggable.java @@ -44,7 +44,7 @@ public interface IDraggable { * @param widget current top most widget below the mouse * @return if the location is valid */ - default boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + default boolean canDropHere(int x, int y, @Nullable IWidget widget) { return true; } diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IGuiElement.java b/src/main/java/com/cleanroommc/modularui/api/widget/IGuiElement.java index 2afb8ced1..5a461e45b 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IGuiElement.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IGuiElement.java @@ -1,13 +1,16 @@ package com.cleanroommc.modularui.api.widget; -import com.cleanroommc.modularui.api.layout.IResizeable; import com.cleanroommc.modularui.screen.ModularScreen; -import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.widget.sizer.Area; +import com.cleanroommc.modularui.widget.sizer.ResizeNode; + +import org.jetbrains.annotations.ApiStatus; /** * Base interface for gui elements. For example widgets. */ +@ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") +@Deprecated public interface IGuiElement { /** @@ -27,7 +30,7 @@ public interface IGuiElement { */ boolean hasParent(); - IResizeable resizer(); + ResizeNode resizer(); /** * @return the area this element occupies @@ -43,13 +46,6 @@ default Area getParentArea() { return getParent().getArea(); } - /** - * Draws this element - * - * @param context gui context - */ - void draw(ModularGuiContext context); - /** * Called when the mouse hovers this element. This means this element is directly below the mouse or there are widgets in between which * all allow to pass hover through. This is not called when the element is at any point below the mouse. diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IPositioned.java b/src/main/java/com/cleanroommc/modularui/api/widget/IPositioned.java index a61cc0d10..51632ab21 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IPositioned.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IPositioned.java @@ -2,9 +2,13 @@ import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.widget.sizer.Area; -import com.cleanroommc.modularui.widget.sizer.Flex; +import com.cleanroommc.modularui.widget.sizer.AreaResizer; +import com.cleanroommc.modularui.widget.sizer.ResizeNode; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; import com.cleanroommc.modularui.widget.sizer.Unit; +import org.jetbrains.annotations.ApiStatus; + import java.util.function.Consumer; import java.util.function.DoubleSupplier; @@ -16,7 +20,7 @@ @SuppressWarnings({"unused", "UnusedReturnValue"}) public interface IPositioned> { - Flex flex(); + StandardResizer resizer(); Area getArea(); @@ -30,12 +34,12 @@ default W getThis() { } default W coverChildrenWidth() { - flex().coverChildrenWidth(); + resizer().coverChildrenWidth(); return getThis(); } default W coverChildrenHeight() { - flex().coverChildrenHeight(); + resizer().coverChildrenHeight(); return getThis(); } @@ -44,292 +48,298 @@ default W coverChildren() { } default W expanded() { - flex().expanded(); + resizer().expanded(); return getThis(); } + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") default W relative(IGuiElement guiElement) { return relative(guiElement.getArea()); } - default W relative(Area guiElement) { - flex().relative(guiElement); - return getThis(); + @Deprecated + default W relative(Area area) { + return relative(new AreaResizer(area)); } - default W relativeToScreen() { - flex().relativeToScreen(); + default W relative(ResizeNode resizeNode) { + resizer().relative(resizeNode); return getThis(); } - default W relativeToParent() { - flex().relativeToParent(); + default W relative(IWidget widget) { + return relative(widget.resizer()); + } + + default W relativeToScreen() { + resizer().relativeToScreen(); return getThis(); } - default W bypassLayerRestriction() { - flex().bypassLayerRestriction(); + default W relativeToParent() { + resizer().relativeToParent(); return getThis(); } default W left(int val) { - flex().left(val, 0, 0, Unit.Measure.PIXEL, true); + resizer().left(val, 0, 0, Unit.Measure.PIXEL, true); return getThis(); } default W leftRel(float val) { - flex().left(val, 0, 0, Unit.Measure.RELATIVE, true); + resizer().left(val, 0, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W leftRelOffset(float val, int offset) { - flex().left(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().left(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W leftRelAnchor(float val, float anchor) { - flex().left(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().left(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W leftRel(float val, int offset, float anchor) { - flex().left(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().left(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W left(float val, int offset, float anchor, Unit.Measure measure) { - flex().left(val, offset, anchor, measure, false); + resizer().left(val, offset, anchor, measure, false); return getThis(); } default W left(DoubleSupplier val, Unit.Measure measure) { - flex().left(val, 0, 0, measure, true); + resizer().left(val, 0, 0, measure, true); return getThis(); } default W leftRelOffset(DoubleSupplier val, int offset) { - flex().left(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().left(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W leftRelAnchor(DoubleSupplier val, float anchor) { - flex().left(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().left(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W leftRel(DoubleSupplier val, int offset, float anchor) { - flex().left(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().left(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W right(int val) { - flex().right(val, 0, 0, Unit.Measure.PIXEL, true); + resizer().right(val, 0, 0, Unit.Measure.PIXEL, true); return getThis(); } default W rightRel(float val) { - flex().right(val, 0, 0, Unit.Measure.RELATIVE, true); + resizer().right(val, 0, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W rightRelOffset(float val, int offset) { - flex().right(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().right(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W rightRelAnchor(float val, float anchor) { - flex().right(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().right(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W rightRel(float val, int offset, float anchor) { - flex().right(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().right(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W right(float val, int offset, float anchor, Unit.Measure measure) { - flex().right(val, offset, anchor, measure, false); + resizer().right(val, offset, anchor, measure, false); return getThis(); } default W right(DoubleSupplier val, Unit.Measure measure) { - flex().right(val, 0, 0, measure, true); + resizer().right(val, 0, 0, measure, true); return getThis(); } default W rightRelOffset(DoubleSupplier val, int offset) { - flex().right(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().right(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W rightRelAnchor(DoubleSupplier val, float anchor) { - flex().right(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().right(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W rightRel(DoubleSupplier val, int offset, float anchor) { - flex().right(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().right(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W top(int val) { - flex().top(val, 0, 0, Unit.Measure.PIXEL, true); + resizer().top(val, 0, 0, Unit.Measure.PIXEL, true); return getThis(); } default W topRel(float val) { - flex().top(val, 0, 0, Unit.Measure.RELATIVE, true); + resizer().top(val, 0, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W topRelOffset(float val, int offset) { - flex().top(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().top(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W topRelAnchor(float val, float anchor) { - flex().top(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().top(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W topRel(float val, int offset, float anchor) { - flex().top(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().top(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W top(float val, int offset, float anchor, Unit.Measure measure) { - flex().top(val, offset, anchor, measure, false); + resizer().top(val, offset, anchor, measure, false); return getThis(); } default W top(DoubleSupplier val, Unit.Measure measure) { - flex().top(val, 0, 0, measure, true); + resizer().top(val, 0, 0, measure, true); return getThis(); } default W topRelOffset(DoubleSupplier val, int offset) { - flex().top(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().top(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W topRelAnchor(DoubleSupplier val, float anchor) { - flex().top(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().top(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W topRel(DoubleSupplier val, int offset, float anchor) { - flex().top(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().top(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W bottom(int val) { - flex().bottom(val, 0, 0, Unit.Measure.PIXEL, true); + resizer().bottom(val, 0, 0, Unit.Measure.PIXEL, true); return getThis(); } default W bottomRel(float val) { - flex().bottom(val, 0, 0, Unit.Measure.RELATIVE, true); + resizer().bottom(val, 0, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W bottomRelOffset(float val, int offset) { - flex().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W bottomRelAnchor(float val, float anchor) { - flex().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W bottomRel(float val, int offset, float anchor) { - flex().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W bottom(float val, int offset, float anchor, Unit.Measure measure) { - flex().bottom(val, offset, anchor, measure, false); + resizer().bottom(val, offset, anchor, measure, false); return getThis(); } default W bottom(DoubleSupplier val, Unit.Measure measure) { - flex().bottom(val, 0, 0, measure, true); + resizer().bottom(val, 0, 0, measure, true); return getThis(); } default W bottomRelOffset(DoubleSupplier val, int offset) { - flex().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); + resizer().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); return getThis(); } default W bottomRelAnchor(DoubleSupplier val, float anchor) { - flex().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); + resizer().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W bottomRel(DoubleSupplier val, int offset, float anchor) { - flex().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); + resizer().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); return getThis(); } default W width(int val) { - flex().width(val, 0, Unit.Measure.PIXEL); + resizer().width(val, 0, Unit.Measure.PIXEL); return getThis(); } default W widthRel(float val) { - flex().width(val, 0, Unit.Measure.RELATIVE); + resizer().width(val, 0, Unit.Measure.RELATIVE); return getThis(); } default W widthRelOffset(float val, int offset) { - flex().width(val, offset, Unit.Measure.RELATIVE); + resizer().width(val, offset, Unit.Measure.RELATIVE); return getThis(); } default W width(float val, Unit.Measure measure) { - flex().width(val, 0, measure); + resizer().width(val, 0, measure); return getThis(); } default W width(DoubleSupplier val, Unit.Measure measure) { - flex().width(val, 0, measure); + resizer().width(val, 0, measure); return getThis(); } default W widthRelOffset(DoubleSupplier val, int offset) { - flex().width(val, offset, Unit.Measure.RELATIVE); + resizer().width(val, offset, Unit.Measure.RELATIVE); return getThis(); } default W height(int val) { - flex().height(val, 0, Unit.Measure.PIXEL); + resizer().height(val, 0, Unit.Measure.PIXEL); return getThis(); } default W heightRel(float val) { - flex().height(val, 0, Unit.Measure.RELATIVE); + resizer().height(val, 0, Unit.Measure.RELATIVE); return getThis(); } default W heightRelOffset(float val, int offset) { - flex().height(val, offset, Unit.Measure.RELATIVE); + resizer().height(val, offset, Unit.Measure.RELATIVE); return getThis(); } default W height(float val, Unit.Measure measure) { - flex().height(val, 0, measure); + resizer().height(val, 0, measure); return getThis(); } default W height(DoubleSupplier val, Unit.Measure measure) { - flex().height(val, 0, measure); + resizer().height(val, 0, measure); return getThis(); } default W heightRelOffset(DoubleSupplier val, int offset) { - flex().height(val, offset, Unit.Measure.RELATIVE); + resizer().height(val, offset, Unit.Measure.RELATIVE); return getThis(); } @@ -374,27 +384,27 @@ default W full() { } default W anchorLeft(float val) { - flex().anchorLeft(val); + resizer().anchorLeft(val); return getThis(); } default W anchorRight(float val) { - flex().anchorRight(val); + resizer().anchorRight(val); return getThis(); } default W anchorTop(float val) { - flex().anchorTop(val); + resizer().anchorTop(val); return getThis(); } default W anchorBottom(float val) { - flex().anchorBottom(val); + resizer().anchorBottom(val); return getThis(); } default W anchor(Alignment alignment) { - flex().anchor(alignment); + resizer().anchor(alignment); return getThis(); } @@ -433,8 +443,8 @@ default W center() { return align(Alignment.Center); } - default W flex(Consumer flexConsumer) { - flexConsumer.accept(flex()); + default W resizer(Consumer flexConsumer) { + flexConsumer.accept(resizer()); return getThis(); } diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java b/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java index 1855481e9..c0e11d6d8 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java @@ -1,14 +1,15 @@ package com.cleanroommc.modularui.api.widget; import com.cleanroommc.modularui.api.ITheme; -import com.cleanroommc.modularui.api.layout.IResizeable; +import com.cleanroommc.modularui.api.ITreeNode; import com.cleanroommc.modularui.api.layout.IViewportStack; import com.cleanroommc.modularui.drawable.Stencil; import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.widget.sizer.Area; -import com.cleanroommc.modularui.widget.sizer.Flex; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -19,94 +20,37 @@ import java.util.function.Consumer; /** - * A widget in a Gui + * A widget in a Gui. */ -public interface IWidget extends IGuiElement { +public interface IWidget extends IGuiElement, ITreeNode { /** - * Validates and initialises this element. - * This element now becomes valid - * - * @param parent the parent this element belongs to - * @param late true if this is called some time after the widget tree of the parent has been initialised - */ - void initialise(@NotNull IWidget parent, boolean late); - - /** - * Invalidates this element. - */ - void dispose(); - - /** - * Determines if this element exist in an active gui. - * - * @return if this is in a valid gui + * @return the screen this element is in */ - boolean isValid(); + ModularScreen getScreen(); /** - * Draws the background of this widget. - * - * @param context gui context - * @param widgetTheme widget theme of this widget + * @return the parent of this widget */ - void drawBackground(ModularGuiContext context, WidgetThemeEntry widgetTheme); + @NotNull + @Override + IWidget getParent(); - /** - * Draws additional stuff in this widget. - * x = 0 and y = 0 is now in the top left corner of this widget. - * Do NOT override this method, it is never called. Use {@link #draw(ModularGuiContext, WidgetThemeEntry)} instead. - * - * @param context gui context - */ - @ApiStatus.NonExtendable - @Deprecated @Override - default void draw(ModularGuiContext context) { - draw(context, getWidgetTheme(context.getTheme())); + default boolean hasParent() { + return isValid(); } /** - * Draws extra elements of this widget. Called after {@link #drawBackground(ModularGuiContext, WidgetThemeEntry)} and before - * {@link #drawOverlay(ModularGuiContext, WidgetThemeEntry)} - * - * @param context gui context - * @param widgetTheme widget theme - */ - void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme); - - /** - * Draws the overlay of this theme. - * - * @param context gui context - * @param widgetTheme widget theme - */ - void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetTheme); - - /** - * Draws foreground elements of this widget. For example tooltips. - * No transformations are applied here. - * - * @param context gui context + * @return the context the current screen */ - void drawForeground(ModularGuiContext context); - - default void transform(IViewportStack stack) { - stack.translate(getArea().rx, getArea().ry, 0); - } - - default Object getAdditionalHoverInfo(IViewportStack viewportStack, int mouseX, int mouseY) { - return null; - } - - default WidgetThemeEntry getWidgetTheme(ITheme theme) { - return theme.getFallback(); - } + ModularGuiContext getContext(); /** - * Called 20 times per second. + * @return the panel this widget is in */ - void onUpdate(); + @NotNull + ModularPanel getPanel(); /** * @return the area this widget occupies @@ -114,6 +58,15 @@ default WidgetThemeEntry getWidgetTheme(ITheme theme) { @Override Area getArea(); + /** + * Shortcut to get the area of the parent + * + * @return parent area + */ + default Area getParentArea() { + return getParent().getArea(); + } + /** * Calculates if a given pos is inside this widgets area. * This should be used over {@link Area#isInside(int, int)}, since this accounts for transformations. @@ -148,49 +101,49 @@ default boolean isInside(IViewportStack stack, int mx, int my, boolean absolute) } /** - * @return all children of this widget + * Called when the mouse hovers this element. This means this element is directly below the mouse or there are widgets in between which + * all allow to pass hover through. This is not called when the element is at any point below the mouse. */ - @NotNull - default List getChildren() { - return Collections.emptyList(); - } + default void onMouseStartHover() {} /** - * @return if this widget has any children + * Called when the mouse no longer hovers this element. This widget can still be below the mouse on some level. */ - default boolean hasChildren() { - return !getChildren().isEmpty(); - } + default void onMouseEndHover() {} /** - * @return the panel this widget is in + * Called when the mouse enters this elements area with any amount of widgets above it from the current panel. */ - @NotNull - ModularPanel getPanel(); + default void onMouseEnterArea() {} /** - * Returns if this element is enabled. Disabled elements are not drawn and can not be interacted with. If this is disabled, the children - * will be considered disabled to without actually being disabled. - * - * @return if this element is enabled + * Called when the mouse leaves the area, or it started hovering a different panel. */ - @Override - boolean isEnabled(); + default void onMouseLeaveArea() {} - void setEnabled(boolean enabled); + /** + * @return if this widget is currently right below the mouse + */ + default boolean isHovering() { + return isHoveringFor(0); + } /** - * Checks if all ancestors are enabled. Only then this widget is visible and interactable. + * Returns if this element is right blow the mouse for a certain amount of time * - * @return if all ancestors are enabled. + * @param ticks time in ticks + * @return if this element is right blow the mouse for a certain amount of time */ - default boolean areAncestorsEnabled() { - IWidget parent = this; - do { - if (!parent.isEnabled()) return false; - parent = parent.getParent(); - } while (parent.hasParent()); - return true; + default boolean isHoveringFor(int ticks) { + return false; + } + + default boolean isBelowMouse() { + return isBelowMouseFor(0); + } + + default boolean isBelowMouseFor(int ticks) { + return false; } /** @@ -228,40 +181,142 @@ default boolean canHoverThrough() { } /** - * Marks tooltip for this widget as dirty. + * @return default width if it can't be calculated */ - void markTooltipDirty(); + default int getDefaultWidth() { + return 18; + } /** - * @return the parent of this widget + * @return default height if it can't be calculated + */ + default int getDefaultHeight() { + return 18; + } + + + /** + * Validates and initialises this element. + * This element now becomes valid + * + * @param parent the parent this element belongs to + * @param late true if this is called some time after the widget tree of the parent has been initialised + */ + void initialise(@NotNull IWidget parent, boolean late); + + /** + * Invalidates this element. + */ + void dispose(); + + /** + * Determines if this element exist in an active gui. + * + * @return if this is in a valid gui + */ + boolean isValid(); + + /** + * Called 20 times per second. + */ + default void onUpdate() {} + + /** + * Draws the background of this widget. + * + * @param context gui context + * @param widgetTheme widget theme of this widget + */ + default void drawBackground(ModularGuiContext context, WidgetThemeEntry widgetTheme) {} + + /** + * Draws extra elements of this widget. Called after {@link #drawBackground(ModularGuiContext, WidgetThemeEntry)} and before + * {@link #drawOverlay(ModularGuiContext, WidgetThemeEntry)} + * + * @param context gui context + * @param widgetTheme widget theme + */ + default void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) {} + + /** + * Draws the overlay of this theme. + * + * @param context gui context + * @param widgetTheme widget theme + */ + default void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetTheme) {} + + /** + * Draws foreground elements of this widget. For example tooltips. + * No transformations are applied here. + * + * @param context gui context + */ + default void drawForeground(ModularGuiContext context) {} + + default void transform(IViewportStack stack) { + stack.translate(getArea().rx, getArea().ry, 0); + } + + default Object getAdditionalHoverInfo(IViewportStack viewportStack, int mouseX, int mouseY) { + return null; + } + + default WidgetThemeEntry getWidgetTheme(ITheme theme) { + return theme.getFallback(); + } + + /** + * @return all children of this widget */ @NotNull - IWidget getParent(); + @Override + default List getChildren() { + return Collections.emptyList(); + } + /** + * @return if this widget has any children + */ @Override - default boolean hasParent() { - return isValid(); + default boolean hasChildren() { + return !getChildren().isEmpty(); } + void scheduleResize(); + + boolean requiresResize(); + /** - * @return the context the current screen + * @return flex of this widget */ - ModularGuiContext getContext(); + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Nullable + default StandardResizer getFlex() { + return resizer(); + } /** * @return flex of this widget. Creates a new one if it doesn't already have one. */ - Flex flex(); + @NotNull + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + default StandardResizer flex() { + return resizer(); + } /** - * Does the same as {@link IPositioned#flex(Consumer)} + * Does the same as {@link IPositioned#resizer(Consumer)} * * @param builder function to build flex * @return this */ - default IWidget flexBuilder(Consumer builder) { - builder.accept(flex()); - return this; + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + default IWidget flexBuilder(Consumer builder) { + return resizerBuilder(builder); } /** @@ -269,14 +324,12 @@ default IWidget flexBuilder(Consumer builder) { */ @NotNull @Override - IResizeable resizer(); + StandardResizer resizer(); - /** - * Sets the resizer of this widget. - * - * @param resizer resizer - */ - void resizer(IResizeable resizer); + default IWidget resizerBuilder(Consumer builder) { + builder.accept(resizer()); + return this; + } /** * Called before a widget is resized. @@ -294,13 +347,28 @@ default void onResized() {} default void postResize() {} /** - * @return flex of this widget + * Returns if this element is enabled. Disabled elements are not drawn and can not be interacted with. If this is disabled, the children + * will be considered disabled to without actually being disabled. + * + * @return if this element is enabled */ - Flex getFlex(); + @Override + boolean isEnabled(); - default boolean isExpanded() { - Flex flex = getFlex(); - return flex != null && flex.isExpanded(); + void setEnabled(boolean enabled); + + /** + * Checks if all ancestors are enabled. Only then this widget is visible and interactable. + * + * @return if all ancestors are enabled. + */ + default boolean areAncestorsEnabled() { + IWidget parent = this; + do { + if (!parent.isEnabled()) return false; + parent = parent.getParent(); + } while (parent.hasParent()); + return true; } @Nullable String getName(); diff --git a/src/main/java/com/cleanroommc/modularui/config/Config.java b/src/main/java/com/cleanroommc/modularui/config/Config.java index a7ad29954..2b99842b3 100644 --- a/src/main/java/com/cleanroommc/modularui/config/Config.java +++ b/src/main/java/com/cleanroommc/modularui/config/Config.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.config; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.network.NetworkHandler; import com.cleanroommc.modularui.network.packets.SyncConfig; import com.cleanroommc.modularui.screen.ModularScreen; @@ -80,7 +81,7 @@ private boolean determineSynced() { } public ModularScreen createScreen() { - return new ModularScreen(new ConfigPanel(this)); + return new ModularScreen(ModularUI.ID, new ConfigPanel(this)); } public JsonObject serialize() { diff --git a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java index a622fbfbf..eff200aeb 100644 --- a/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java +++ b/src/main/java/com/cleanroommc/modularui/drawable/graph/Plot.java @@ -1,13 +1,11 @@ package com.cleanroommc.modularui.drawable.graph; -import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.drawable.GuiDraw; import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.DAM; import com.cleanroommc.modularui.utils.Interpolations; import com.cleanroommc.modularui.utils.MathUtils; -import com.cleanroommc.modularui.utils.NumberFormat; import com.cleanroommc.modularui.utils.Platform; public class Plot { @@ -37,7 +35,6 @@ public void redraw() { } private void redraw(GraphView view) { - long time = System.nanoTime(); float dHalf = thickness * 0.5f; int n = xs.length * 4; // each point has 2 offset vertices and each vertex has an x and y component @@ -139,8 +136,6 @@ private void redraw(GraphView view) { lpoy = py * dHalf; storePoints(vertexIndex, view, x1, y1, lpox, lpoy); - time = System.nanoTime() - time; - ModularUI.LOGGER.error("Calculating vertices from {} data points took {}s", xs.length, NumberFormat.formatNanos(time)); } private int storePoints(int index, GraphView view, float sx, float sy, float ox, float oy) { @@ -167,12 +162,9 @@ public void draw(GraphView view) { int a = Color.getAlpha(color); Platform.setupDrawColor(); Platform.startDrawing(Platform.DrawMode.TRIANGLE_STRIP, Platform.VertexFormat.POS_COLOR, buffer -> { - long time = System.nanoTime(); for (int i = 0; i < this.vertexBuffer.length; i += 2) { buffer.pos(this.vertexBuffer[i], this.vertexBuffer[i + 1], 0).color(r, g, b, a).endVertex(); } - time = System.nanoTime() - time; - ModularUI.LOGGER.error("Drawing plot with {} points took {}s", xs.length, NumberFormat.formatNanos(time)); }); } diff --git a/src/main/java/com/cleanroommc/modularui/integration/jei/GhostIngredientTarget.java b/src/main/java/com/cleanroommc/modularui/integration/jei/GhostIngredientTarget.java index 61fe74606..4b1a040ca 100644 --- a/src/main/java/com/cleanroommc/modularui/integration/jei/GhostIngredientTarget.java +++ b/src/main/java/com/cleanroommc/modularui/integration/jei/GhostIngredientTarget.java @@ -1,6 +1,5 @@ package com.cleanroommc.modularui.integration.jei; -import com.cleanroommc.modularui.api.widget.IGuiElement; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerGhostIngredientSlot; @@ -14,11 +13,11 @@ @Optional.Interface(iface = "mezz.jei.api.gui.IGhostIngredientHandler$Target", modid = "jei") public class GhostIngredientTarget implements IGhostIngredientHandler.Target { - private final IGuiElement guiElement; + private final IWidget widget; private final RecipeViewerGhostIngredientSlot ghostSlot; public static GhostIngredientTarget of(RecipeViewerGhostIngredientSlot slot) { - if (slot instanceof IGuiElement guiElement) { + if (slot instanceof IWidget guiElement) { return new GhostIngredientTarget<>(guiElement, slot); } throw new IllegalArgumentException(); @@ -28,14 +27,14 @@ public static > GhostI return new GhostIngredientTarget<>(slot, slot); } - public GhostIngredientTarget(IGuiElement guiElement, RecipeViewerGhostIngredientSlot ghostSlot) { - this.guiElement = guiElement; + public GhostIngredientTarget(IWidget widget, RecipeViewerGhostIngredientSlot ghostSlot) { + this.widget = widget; this.ghostSlot = ghostSlot; } @Override public @NotNull Rectangle getArea() { - return this.guiElement.getArea(); + return this.widget.getArea(); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/integration/jei/ModularScreenJEIHandler.java b/src/main/java/com/cleanroommc/modularui/integration/jei/ModularScreenJEIHandler.java index e640a26c3..071479e49 100644 --- a/src/main/java/com/cleanroommc/modularui/integration/jei/ModularScreenJEIHandler.java +++ b/src/main/java/com/cleanroommc/modularui/integration/jei/ModularScreenJEIHandler.java @@ -1,7 +1,7 @@ package com.cleanroommc.modularui.integration.jei; import com.cleanroommc.modularui.api.IMuiScreen; -import com.cleanroommc.modularui.api.widget.IGuiElement; +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.integration.recipeviewer.RecipeViewerIngredientProvider; import net.minecraft.client.gui.GuiScreen; @@ -83,7 +83,7 @@ public List getGuiExtraAreas(@NotNull T guiContainer) { @Nullable @Override public Object getIngredientUnderMouse(@NotNull T guiContainer, int mouseX, int mouseY) { - IGuiElement hovered = guiContainer.getScreen().getContext().getTopHovered(); + IWidget hovered = guiContainer.getScreen().getContext().getTopHovered(); return hovered instanceof RecipeViewerIngredientProvider jip ? jip.getIngredient() : null; } } diff --git a/src/main/java/com/cleanroommc/modularui/overlay/DebugOptions.java b/src/main/java/com/cleanroommc/modularui/overlay/DebugOptions.java new file mode 100644 index 000000000..926511077 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/overlay/DebugOptions.java @@ -0,0 +1,30 @@ +package com.cleanroommc.modularui.overlay; + +import com.cleanroommc.modularui.utils.Color; +import com.cleanroommc.modularui.value.BoolValue; +import com.cleanroommc.modularui.value.FloatValue; +import com.cleanroommc.modularui.value.IntValue; + +public class DebugOptions { + + public static final DebugOptions INSTANCE = new DebugOptions(); + + public BoolValue showHovered = new BoolValue(true); + public BoolValue showPos = new BoolValue(true); + public BoolValue showSize = new BoolValue(true); + public BoolValue showWidgetTheme = new BoolValue(true); + public BoolValue showExtra = new BoolValue(true); + public BoolValue showOutline = new BoolValue(true); + + public BoolValue showParent = new BoolValue(true); + public BoolValue showParentPos = new BoolValue(true); + public BoolValue showParentSize = new BoolValue(true); + public BoolValue showParentWidgetTheme = new BoolValue(false); + public BoolValue showParentOutline = new BoolValue(true); + + public IntValue textColor = new IntValue(Color.argb(180, 40, 115, 220)); + public IntValue outlineColor = new IntValue(textColor.getIntValue()); + public IntValue cursorColor = new IntValue(Color.withAlpha(Color.GREEN.main, 0.8f)); + public FloatValue scale = new FloatValue(0.8f); + +} diff --git a/src/main/java/com/cleanroommc/modularui/overlay/DebugOverlay.java b/src/main/java/com/cleanroommc/modularui/overlay/DebugOverlay.java new file mode 100644 index 000000000..8425fb1e0 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/overlay/DebugOverlay.java @@ -0,0 +1,128 @@ +package com.cleanroommc.modularui.overlay; + +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.api.IMuiScreen; +import com.cleanroommc.modularui.api.drawable.IIcon; +import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.api.value.IBoolValue; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.drawable.GuiTextures; +import com.cleanroommc.modularui.drawable.NamedDrawableRow; +import com.cleanroommc.modularui.drawable.Rectangle; +import com.cleanroommc.modularui.screen.CustomModularScreen; +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; +import com.cleanroommc.modularui.utils.Color; +import com.cleanroommc.modularui.utils.TreeUtil; +import com.cleanroommc.modularui.widget.WidgetTree; +import com.cleanroommc.modularui.widgets.ButtonWidget; +import com.cleanroommc.modularui.widgets.ListWidget; +import com.cleanroommc.modularui.widgets.ToggleButton; +import com.cleanroommc.modularui.widgets.menu.ContextMenuButton; +import com.cleanroommc.modularui.widgets.menu.Menu; + +import org.jetbrains.annotations.NotNull; + +public class DebugOverlay extends CustomModularScreen { + + private static final IIcon CHECKMARK = GuiTextures.CHECKMARK.asIcon().size(8); + + private final IMuiScreen parent; + + public DebugOverlay(IMuiScreen screen) { + super(ModularUI.ID); + this.parent = screen; + } + + @Override + public @NotNull ModularPanel buildUI(ModularGuiContext context) { + return new ModularPanel("debug") + .fullScreenInvisible() + .child(new ContextMenuButton<>("menu_debug_options") + .horizontalCenter() + .bottom(0) + .height(12) + .width(160) + .background(new Rectangle().color(Color.withAlpha(DebugOptions.INSTANCE.outlineColor.getIntValue(), 0.4f)).cornerRadius(4)) + .disableHoverBackground() + .overlay(IKey.str("Debug Options")) + .openUp() + .menuList(l1 -> l1 + .name("menu_list") + .maxSize(100) + .widthRel(1f) + .child(new ButtonWidget<>().name("print_widget_tree_button") + .height(12) + .widthRel(1f) + .invisible() + .overlay(IKey.str("Print widget trees")) + .onMousePressed(this::logWidgetTrees)) + .child(new ButtonWidget<>().name("print_resizer_tree_button") + .height(12) + .widthRel(1f) + .invisible() + .overlay(IKey.str("Print resizer tree")) + .onMousePressed(b -> { + TreeUtil.print(parent.getScreen().getResizeNode()); + return true; + })) + .child(new ContextMenuButton<>("menu_hover_info") + .height(10) + .widthRel(1f) + .overlay(IKey.str("Widget hover info")) + .openRightUp() + .menu(new Menu<>() + .width(100) + .coverChildrenHeight() + .padding(2) + .child(new ListWidget<>() + .maxSize(100) + .widthRel(1f) + .child(toggleOption(0, "Any", DebugOptions.INSTANCE.showHovered)) + .child(toggleOption(1, "Pos", DebugOptions.INSTANCE.showPos)) + .child(toggleOption(2, "Size", DebugOptions.INSTANCE.showSize)) + .child(toggleOption(3, "Widget Theme", DebugOptions.INSTANCE.showWidgetTheme)) + .child(toggleOption(4, "Extra info", DebugOptions.INSTANCE.showExtra)) + .child(toggleOption(5, "Outline", DebugOptions.INSTANCE.showOutline))))) + .child(new ContextMenuButton<>("menu_parent_hover_info") + .name("menu_button_parent_hover_info") + .height(10) + .widthRel(1f) + .overlay(IKey.str("Parent widget hover info")) + .openRightUp() + .menu(new Menu<>() + .width(100) + .coverChildrenHeight() + .padding(2) + .child(new ListWidget<>() + .maxSize(100) + .widthRel(1f) + .child(toggleOption(10, "Any", DebugOptions.INSTANCE.showParent)) + .child(toggleOption(11, "Pos", DebugOptions.INSTANCE.showParentPos)) + .child(toggleOption(12, "Size", DebugOptions.INSTANCE.showParentSize)) + .child(toggleOption(13, "Widget Theme", DebugOptions.INSTANCE.showParentWidgetTheme)) + .child(toggleOption(14, "Outline", DebugOptions.INSTANCE.showParentOutline)) + ))))); + } + + public static IWidget toggleOption(int i, String name, IBoolValue boolValue) { + return new ToggleButton() + .name("hover_info_toggle" + i) + .invisible() + .widthRel(1f) + .height(12) + .value(boolValue) + .overlay(true, new NamedDrawableRow() + .name(IKey.str(name)) + .drawable(CHECKMARK)) + .overlay(false, new NamedDrawableRow() + .name(IKey.str(name))); + } + + private boolean logWidgetTrees(int b) { + for (ModularPanel panel : parent.getScreen().getPanelManager().getOpenPanels()) { + WidgetTree.print(panel); + } + return true; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/overlay/OverlayHandler.java b/src/main/java/com/cleanroommc/modularui/overlay/OverlayHandler.java index 0b988e740..bb9769ac0 100644 --- a/src/main/java/com/cleanroommc/modularui/overlay/OverlayHandler.java +++ b/src/main/java/com/cleanroommc/modularui/overlay/OverlayHandler.java @@ -10,7 +10,8 @@ import java.util.function.Function; import java.util.function.Predicate; -@ApiStatus.Experimental +@Deprecated +@ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") public class OverlayHandler implements Comparable { private final Predicate test; diff --git a/src/main/java/com/cleanroommc/modularui/overlay/OverlayManager.java b/src/main/java/com/cleanroommc/modularui/overlay/OverlayManager.java index fed88d840..62aca1a8e 100644 --- a/src/main/java/com/cleanroommc/modularui/overlay/OverlayManager.java +++ b/src/main/java/com/cleanroommc/modularui/overlay/OverlayManager.java @@ -1,17 +1,12 @@ package com.cleanroommc.modularui.overlay; -import com.cleanroommc.modularui.screen.ModularScreen; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; - import org.jetbrains.annotations.ApiStatus; import java.util.ArrayList; import java.util.List; -import java.util.Objects; -@ApiStatus.Experimental +@Deprecated +@ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") public class OverlayManager { public static final List overlays = new ArrayList<>(); @@ -22,18 +17,4 @@ public static void register(OverlayHandler handler) { overlays.sort(OverlayHandler::compareTo); } } - - public static void onGuiOpen(GuiScreen newScreen) { - if (newScreen != Minecraft.getMinecraft().currentScreen) { - OverlayStack.closeAll(); - if (newScreen == null) return; - for (OverlayHandler handler : overlays) { - if (handler.isValidFor(newScreen)) { - ModularScreen overlay = Objects.requireNonNull(handler.createOverlay(newScreen), "Overlays must not be null!"); - overlay.constructOverlay(newScreen); - OverlayStack.open(overlay); - } - } - } - } } diff --git a/src/main/java/com/cleanroommc/modularui/overlay/OverlayStack.java b/src/main/java/com/cleanroommc/modularui/overlay/OverlayStack.java index 50d2ecaa4..a25d9bd33 100644 --- a/src/main/java/com/cleanroommc/modularui/overlay/OverlayStack.java +++ b/src/main/java/com/cleanroommc/modularui/overlay/OverlayStack.java @@ -1,20 +1,26 @@ package com.cleanroommc.modularui.overlay; -import com.cleanroommc.modularui.api.widget.IGuiElement; +import com.cleanroommc.modularui.ModularUIConfig; +import com.cleanroommc.modularui.api.IMuiScreen; +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.ClientScreenHandler; import com.cleanroommc.modularui.screen.ModularScreen; +import com.cleanroommc.modularui.screen.OpenScreenEvent; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.renderer.GlStateManager; +import net.minecraftforge.common.MinecraftForge; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; -@ApiStatus.Experimental +@ApiStatus.Internal public class OverlayStack { private static final List overlay = new ArrayList<>(); @@ -96,10 +102,10 @@ public static void onTick() { } @Nullable - public static IGuiElement getHoveredElement() { + public static IWidget getHoveredElement() { for (int i = overlay.size() - 1; i >= 0; i--) { ModularScreen screen = overlay.get(i); - IGuiElement hovered = screen.getContext().getTopHovered(); + IWidget hovered = screen.getContext().getTopHovered(); if (hovered == null) continue; return hovered; } @@ -109,4 +115,30 @@ public static IGuiElement getHoveredElement() { public static boolean isHoveringOverlay() { return getHoveredElement() != null; } + + public static void onGuiOpen(GuiScreen newScreen) { + closeAll(); + if (newScreen != null) { + // backwards compat + for (OverlayHandler handler : OverlayManager.overlays) { + if (handler.isValidFor(newScreen)) { + ModularScreen overlay = Objects.requireNonNull(handler.createOverlay(newScreen), "Overlays must not be null!"); + overlay.constructOverlay(newScreen); + OverlayStack.open(overlay); + } + } + + OpenScreenEvent event = new OpenScreenEvent(newScreen); + MinecraftForge.EVENT_BUS.post(event); + for (ModularScreen overlay : event.getOverlays()) { + overlay.constructOverlay(newScreen); + open(overlay); + } + if (ModularUIConfig.guiDebugMode && newScreen instanceof IMuiScreen muiScreen) { + ModularScreen overlay = new DebugOverlay(muiScreen); + overlay.constructOverlay(newScreen); + open(overlay); + } + } + } } diff --git a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java index bff940fcf..214b51790 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ClientScreenHandler.java @@ -6,7 +6,6 @@ import com.cleanroommc.modularui.api.IMuiScreen; import com.cleanroommc.modularui.api.MCHelper; import com.cleanroommc.modularui.api.UpOrDown; -import com.cleanroommc.modularui.api.widget.IGuiElement; import com.cleanroommc.modularui.api.widget.IVanillaSlot; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.core.mixins.early.minecraft.GuiAccessor; @@ -16,7 +15,7 @@ import com.cleanroommc.modularui.drawable.Stencil; import com.cleanroommc.modularui.integration.jei.ModularUIJeiPlugin; import com.cleanroommc.modularui.network.ModularNetwork; -import com.cleanroommc.modularui.overlay.OverlayManager; +import com.cleanroommc.modularui.overlay.DebugOptions; import com.cleanroommc.modularui.overlay.OverlayStack; import com.cleanroommc.modularui.screen.viewport.GuiContext; import com.cleanroommc.modularui.screen.viewport.LocatedWidget; @@ -225,7 +224,7 @@ private static void onGuiChanged(GuiScreen oldScreen, GuiScreen newScreen) { ModularNetwork.CLIENT.closeAll(); } - OverlayManager.onGuiOpen(newScreen); + OverlayStack.onGuiOpen(newScreen); } private static void invalidateCurrentScreen() { @@ -331,7 +330,7 @@ private static boolean handleKeyboardInput(@Nullable ModularScreen muiScreen, Gu } else { // releasing a key // for some reason when you press E after joining a world the button will not trigger the press event, - // but ony the release event, causing this to be null + // but only the release event, causing this to be null if (lastChar == null) return false; // when the key is released, the event char is empty if (inputPhase.isEarly() && doAction(muiScreen, ms -> ms.onKeyRelease(lastChar, key))) { @@ -477,7 +476,7 @@ public static void drawContainer(ModularScreen muiScreen, GuiContainer mcScreen, muiScreen.drawForeground(); acc.setHoveredSlot(null); - IGuiElement hovered = muiScreen.getContext().getTopHovered(); + IWidget hovered = muiScreen.getContext().getTopHovered(); if (hovered instanceof IVanillaSlot vanillaSlot && vanillaSlot.handleAsVanillaSlot()) { acc.setHoveredSlot(vanillaSlot.getVanillaSlot()); } @@ -575,18 +574,22 @@ public static void drawDebugScreen(@Nullable ModularScreen muiScreen, @Nullable ModularGuiContext context = muiScreen.getContext(); int mouseX = context.getAbsMouseX(), mouseY = context.getAbsMouseY(); int screenH = muiScreen.getScreenArea().height; - int color = Color.argb(180, 40, 115, 220); - float scale = 0.80f; + int outlineColor = DebugOptions.INSTANCE.outlineColor.getIntValue();//Color.argb(180, 40, 115, 220); + int textColor = DebugOptions.INSTANCE.textColor.getIntValue();//Color.argb(180, 40, 115, 220); + float scale = DebugOptions.INSTANCE.scale.getFloatValue(); int shift = (int) (11 * scale + 0.5f); int lineY = screenH - shift - 2; - GuiDraw.drawText("Mouse Pos: " + mouseX + ", " + mouseY, 5, lineY, scale, color, true); + if (ModularUI.Mods.JEI.isLoaded()) lineY -= 12; + GuiDraw.drawText("Mouse Pos: " + mouseX + ", " + mouseY, 5, lineY, scale, outlineColor, true); lineY -= shift; - GuiDraw.drawText("FPS: " + fpsCounter.getFps(), 5, lineY, scale, color, true); + GuiDraw.drawText("FPS: " + fpsCounter.getFps(), 5, lineY, scale, outlineColor, true); lineY -= shift; - GuiDraw.drawText("Theme ID: " + context.getTheme().getId(), 5, lineY, scale, color, true); + GuiDraw.drawText("Theme ID: " + context.getTheme().getId(), 5, lineY, scale, outlineColor, true); LocatedWidget locatedHovered = muiScreen.getPanelManager().getTopWidgetLocated(true); - if (locatedHovered != null) { - drawSegmentLine(lineY -= 4, scale, color); + boolean showHovered = DebugOptions.INSTANCE.showHovered.getBoolValue(); + boolean showParent = DebugOptions.INSTANCE.showParent.getBoolValue(); + if (locatedHovered != null && (showHovered || showParent)) { + drawSegmentLine(lineY -= 4, scale, outlineColor); lineY -= 10; IWidget hovered = locatedHovered.getElement(); @@ -597,53 +600,75 @@ public static void drawDebugScreen(@Nullable ModularScreen muiScreen, @Nullable Area area = hovered.getArea(); IWidget parent = hovered.getParent(); - GuiDraw.drawBorderOutsideXYWH(0, 0, area.width, area.height, scale, color); - if (hovered.hasParent()) { - GuiDraw.drawBorderOutsideXYWH(-area.rx, -area.ry, parent.getArea().width, parent.getArea().height, scale, Color.withAlpha(color, 0.3f)); + if (showHovered && DebugOptions.INSTANCE.showOutline.getBoolValue()) { + GuiDraw.drawBorderOutsideXYWH(0, 0, area.width, area.height, scale, outlineColor); + } + if (hovered.hasParent() && showParent && DebugOptions.INSTANCE.showParentOutline.getBoolValue()) { + GuiDraw.drawBorderOutsideXYWH(-area.rx, -area.ry, parent.getArea().width, parent.getArea().height, scale, Color.withAlpha(outlineColor, 0.3f)); } GlStateManager.popMatrix(); locatedHovered.unapplyMatrix(context); - GuiDraw.drawText("Widget Theme: " + hovered.getWidgetTheme(muiScreen.getCurrentTheme()).getKey().getFullName(), 5, lineY, scale, color, true); - lineY -= shift; - GuiDraw.drawText("Size: " + area.width + ", " + area.height, 5, lineY, scale, color, true); - lineY -= shift; - GuiDraw.drawText("Pos: " + area.x + ", " + area.y + " Rel: " + area.rx + ", " + area.ry, 5, lineY, scale, color, true); - lineY -= shift; - GuiDraw.drawText("Class: " + hovered, 5, lineY, scale, color, true); - if (hovered.hasParent()) { - drawSegmentLine(lineY -= 4, scale, color); - lineY -= 10; - GuiDraw.drawText("Widget Theme: " + parent.getWidgetTheme(muiScreen.getCurrentTheme()).getKey().getFullName(), 5, lineY, scale, color, true); - lineY -= shift; + if (showHovered) { + if (DebugOptions.INSTANCE.showWidgetTheme.getBoolValue()) { + GuiDraw.drawText("Widget Theme: " + hovered.getWidgetTheme(muiScreen.getCurrentTheme()).getKey().getFullName(), 5, lineY, scale, textColor, true); + lineY -= shift; + } + if (DebugOptions.INSTANCE.showSize.getBoolValue()) { + GuiDraw.drawText("Size: " + area.width + ", " + area.height, 5, lineY, scale, textColor, true); + lineY -= shift; + } + if (DebugOptions.INSTANCE.showPos.getBoolValue()) { + GuiDraw.drawText("Pos: " + area.x + ", " + area.y + " Rel: " + area.rx + ", " + area.ry, 5, lineY, scale, textColor, true); + lineY -= shift; + } + GuiDraw.drawText("Widget: " + hovered, 5, lineY, scale, textColor, true); + } + if (hovered.hasParent() && showParent) { + if (showHovered) { + drawSegmentLine(lineY -= 4, scale, textColor); + lineY -= 10; + } + if (DebugOptions.INSTANCE.showParentWidgetTheme.getBoolValue()) { + GuiDraw.drawText("Widget Theme: " + parent.getWidgetTheme(muiScreen.getCurrentTheme()).getKey().getFullName(), 5, lineY, scale, textColor, true); + lineY -= shift; + } area = parent.getArea(); - GuiDraw.drawText("Parent size: " + area.width + ", " + area.height, 5, lineY, scale, color, true); - lineY -= shift; - GuiDraw.drawText("Parent: " + parent, 5, lineY, scale, color, true); + if (DebugOptions.INSTANCE.showParentSize.getBoolValue()) { + GuiDraw.drawText("Parent size: " + area.width + ", " + area.height, 5, lineY, scale, textColor, true); + lineY -= shift; + } + if (DebugOptions.INSTANCE.showParentPos.getBoolValue()) { + GuiDraw.drawText("Parent pos: " + area.x + ", " + area.y + " Rel: " + area.rx + ", " + area.ry, 5, lineY, scale, textColor, true); + lineY -= shift; + } + GuiDraw.drawText("Parent: " + parent, 5, lineY, scale, outlineColor, true); } - if (hovered instanceof ItemSlot slotWidget) { - drawSegmentLine(lineY -= 4, scale, color); - lineY -= 10; - ModularSlot slot = slotWidget.getSlot(); - GuiDraw.drawText("Slot Index: " + slot.getSlotIndex(), 5, lineY, scale, color, false); - lineY -= shift; - GuiDraw.drawText("Slot Number: " + slot.slotNumber, 5, lineY, scale, color, false); - lineY -= shift; - if (slotWidget.isSynced()) { - SlotGroup slotGroup = slot.getSlotGroup(); - boolean allowShiftTransfer = slotGroup != null && slotGroup.allowShiftTransfer(); - GuiDraw.drawText("Shift-Click Priority: " + (allowShiftTransfer ? slotGroup.getShiftClickPriority() : "DISABLED"), 5, lineY, scale, color, true); + if (showHovered && DebugOptions.INSTANCE.showExtra.getBoolValue()) { + if (hovered instanceof ItemSlot slotWidget) { + drawSegmentLine(lineY -= 4, scale, textColor); + lineY -= 10; + ModularSlot slot = slotWidget.getSlot(); + GuiDraw.drawText("Slot Index: " + slot.getSlotIndex(), 5, lineY, scale, textColor, false); + lineY -= shift; + GuiDraw.drawText("Slot Number: " + slot.slotNumber, 5, lineY, scale, textColor, false); + lineY -= shift; + if (slotWidget.isSynced()) { + SlotGroup slotGroup = slot.getSlotGroup(); + boolean allowShiftTransfer = slotGroup != null && slotGroup.allowShiftTransfer(); + GuiDraw.drawText("Shift-Click Priority: " + (allowShiftTransfer ? slotGroup.getShiftClickPriority() : "DISABLED"), 5, lineY, scale, textColor, true); + } + } else if (hovered instanceof RichTextWidget richTextWidget) { + drawSegmentLine(lineY -= 4, scale, outlineColor); + lineY -= 10; + locatedHovered.applyMatrix(context); + Object hoveredElement = richTextWidget.getHoveredElement(); + locatedHovered.unapplyMatrix(context); + GuiDraw.drawText("Hovered: " + hoveredElement, 5, lineY, scale, textColor, true); } - } else if (hovered instanceof RichTextWidget richTextWidget) { - drawSegmentLine(lineY -= 4, scale, color); - lineY -= 10; - locatedHovered.applyMatrix(context); - Object hoveredElement = richTextWidget.getHoveredElement(); - locatedHovered.unapplyMatrix(context); - GuiDraw.drawText("Hovered: " + hoveredElement, 5, lineY, scale, color, true); } } // dot at mouse pos - GuiDraw.drawRect(mouseX, mouseY, 1, 1, Color.withAlpha(Color.GREEN.main, 0.8f)); + GuiDraw.drawRect(mouseX, mouseY, 1, 1, DebugOptions.INSTANCE.cursorColor.getIntValue()); GlStateManager.color(1f, 1f, 1f, 1f); } diff --git a/src/main/java/com/cleanroommc/modularui/screen/CustomModularScreen.java b/src/main/java/com/cleanroommc/modularui/screen/CustomModularScreen.java index 647f93474..dd0f9aeb0 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/CustomModularScreen.java +++ b/src/main/java/com/cleanroommc/modularui/screen/CustomModularScreen.java @@ -18,8 +18,13 @@ public abstract class CustomModularScreen extends ModularScreen { /** * Creates a new screen with ModularUI as its owner. */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated public CustomModularScreen() { super(ModularUI.ID); + if (ModularUI.isDev) { + ModularUI.LOGGER.error("The single arg ModularScreen constructor should not be used. Use the other one and pass in your mod id."); + } } /** diff --git a/src/main/java/com/cleanroommc/modularui/screen/DraggablePanelWrapper.java b/src/main/java/com/cleanroommc/modularui/screen/DraggablePanelWrapper.java index 329f3c54e..ad7a313a1 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/DraggablePanelWrapper.java +++ b/src/main/java/com/cleanroommc/modularui/screen/DraggablePanelWrapper.java @@ -48,10 +48,9 @@ public void onDragEnd(boolean successful) { float x = this.panel.getContext().getAbsMouseX() - this.relativeClickX; y = y / (this.panel.getScreen().getScreenArea().height - this.panel.getArea().height); x = x / (this.panel.getScreen().getScreenArea().width - this.panel.getArea().width); - this.panel.flex().resetPosition(); - this.panel.flex().relativeToScreen(); - this.panel.flex().topRelAnchor(y, y) - .leftRelAnchor(x, x); + this.panel.resizer().resetPosition(); + this.panel.resizer().relativeToScreen(); + this.panel.resizer().topRelAnchor(y, y).leftRelAnchor(x, x); this.panel.scheduleResize(); } } diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java index b6e48ff47..d70d56f7c 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java @@ -4,6 +4,7 @@ import com.cleanroommc.modularui.animation.Animator; import com.cleanroommc.modularui.api.IPanelHandler; import com.cleanroommc.modularui.api.ITheme; +import com.cleanroommc.modularui.api.IThemeApi; import com.cleanroommc.modularui.api.MCHelper; import com.cleanroommc.modularui.api.UpOrDown; import com.cleanroommc.modularui.api.layout.IViewport; @@ -89,6 +90,8 @@ public static ModularPanel defaultPanel(@NotNull String name, int width, int hei private Animator animator; private boolean resizable = false; + private String themeOverride; + private ITheme theme; private Runnable onCloseAction; @@ -234,6 +237,7 @@ public boolean canHover() { public void onOpen(ModularScreen screen) { this.screen = screen; getArea().z(1); + resizer().initialize(this.screen.getResizeNode(), this.screen.getResizeNode()); initialise(this, false); // call first tick after everything is initialised WidgetTree.onUpdate(this); @@ -785,9 +789,16 @@ void closeClientSubPanels() { } } + public ITheme getTheme() { + if (this.theme == null) { + this.theme = IThemeApi.get().getThemeForScreen(this, this.themeOverride); + } + return this.theme; + } + @Override public boolean isExcludeAreaInRecipeViewer() { - return super.isExcludeAreaInRecipeViewer() || (!getScreen().isOverlay() && !this.invisible && !flex().isFullSize()); + return super.isExcludeAreaInRecipeViewer() || (!getScreen().isOverlay() && !this.invisible && !resizer().isFullSize()); } public ModularPanel bindPlayerInventory() { @@ -818,6 +829,12 @@ public ModularPanel onCloseAction(Runnable onCloseAction) { return this; } + public ModularPanel themeOverride(String id) { + this.themeOverride = id; + this.theme = null; + return this; + } + @Deprecated @Override public ModularPanel name(String name) { diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java b/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java index c0ae0dbc1..30db01e8f 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java @@ -15,6 +15,8 @@ import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.widget.WidgetTree; import com.cleanroommc.modularui.widget.sizer.Area; +import com.cleanroommc.modularui.widget.sizer.ResizeNode; +import com.cleanroommc.modularui.widget.sizer.ScreenResizeNode; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; @@ -30,6 +32,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import mezz.jei.gui.ghost.GhostIngredientDrag; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -77,20 +80,25 @@ public static ModularScreen getCurrent() { private final ModularGuiContext context = new ModularGuiContext(this); private final Map, List> guiActionListeners = new Object2ObjectOpenHashMap<>(); private final Object2ObjectArrayMap frameUpdates = new Object2ObjectArrayMap<>(); + private final ScreenResizeNode resizeNode = new ScreenResizeNode(this); private boolean pausesGame = false; private boolean openParentOnClose = false; + private String themeOverride; private ITheme currentTheme; private IMuiScreen screenWrapper; private boolean overlay = false; /** - * Creates a new screen with a ModularUI as its owner and a given {@link ModularPanel}. - * - * @param mainPanel main panel of this screen + * @deprecated use the other constructor */ + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") public ModularScreen(@NotNull ModularPanel mainPanel) { this(ModularUI.ID, mainPanel); + if (ModularUI.isDev) { + ModularUI.LOGGER.warn("The single arg ModularScreen constructor should not be used. Use the any of the other ones and pass in your mod id."); + } } /** @@ -179,9 +187,8 @@ public void onResize(int width, int height) { } this.context.pushViewport(null, this.context.getScreenArea()); - for (ModularPanel panel : this.panelManager.getReverseOpenPanels()) { - WidgetTree.resizeInternal(panel, true); - } + WidgetTree.verifyTree(this.resizeNode, new ReferenceOpenHashSet<>()); + WidgetTree.resizeInternal(this.resizeNode, true); this.context.popViewport(null); if (!isOverlay()) { @@ -599,6 +606,10 @@ public IMuiScreen getScreenWrapper() { return this.screenWrapper; } + public ResizeNode getResizeNode() { + return resizeNode; + } + public Area getScreenArea() { return this.context.getScreenArea(); } @@ -706,9 +717,14 @@ private static Class getGuiActionClass(IGuiAction action) { throw new IllegalArgumentException(); } + @Nullable + public String getThemeOverride() { + return themeOverride; + } + public ITheme getCurrentTheme() { if (this.currentTheme == null) { - useTheme(null); + useTheme(this.themeOverride); } return this.currentTheme; } @@ -721,7 +737,8 @@ public ITheme getCurrentTheme() { * @return this for builder like usage */ public ModularScreen useTheme(String theme) { - this.currentTheme = IThemeApi.get().getThemeForScreen(this, theme); + this.themeOverride = theme; + this.currentTheme = IThemeApi.get().getThemeForScreen(this, this.themeOverride); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/screen/OpenScreenEvent.java b/src/main/java/com/cleanroommc/modularui/screen/OpenScreenEvent.java new file mode 100644 index 000000000..dd633bc98 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/screen/OpenScreenEvent.java @@ -0,0 +1,41 @@ +package com.cleanroommc.modularui.screen; + +import com.cleanroommc.modularui.api.IMuiScreen; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.fml.common.eventhandler.Event; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class OpenScreenEvent extends Event { + + private final GuiScreen screen; + private final List overlays = new ArrayList<>(); + + public OpenScreenEvent(GuiScreen screen) { + this.screen = screen; + } + + public GuiScreen getScreen() { + return screen; + } + + public boolean isModularScreen() { + return screen instanceof IMuiScreen; + } + + public @Nullable ModularScreen getModularScreen() { + return screen instanceof IMuiScreen muiScreen ? muiScreen.getScreen() : null; + } + + public List getOverlays() { + return overlays; + } + + public void addOverlay(ModularScreen screen) { + this.overlays.add(screen); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java b/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java index 21390b66d..39b23055d 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java +++ b/src/main/java/com/cleanroommc/modularui/screen/PanelManager.java @@ -99,7 +99,7 @@ private void openPanel(ModularPanel panel, boolean resize) { this.dirty = true; panel.onOpen(this.screen); if (resize) { - WidgetTree.resizeInternal(panel, true); + WidgetTree.resizeInternal(panel.resizer(), true); } } diff --git a/src/main/java/com/cleanroommc/modularui/screen/viewport/GuiContext.java b/src/main/java/com/cleanroommc/modularui/screen/viewport/GuiContext.java index 3b76986d0..e998bd24e 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/viewport/GuiContext.java +++ b/src/main/java/com/cleanroommc/modularui/screen/viewport/GuiContext.java @@ -3,13 +3,12 @@ import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.api.MCHelper; import com.cleanroommc.modularui.api.drawable.IDrawable; -import com.cleanroommc.modularui.api.widget.IGuiElement; +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.ClientScreenHandler; import com.cleanroommc.modularui.widget.sizer.Area; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; - import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; @@ -46,14 +45,14 @@ public static GuiContext getDefault() { private long tick = 0; private int currentDrawingZ = 0; - public boolean isAbove(IGuiElement widget) { + public boolean isAbove(IWidget widget) { return isMouseAbove(widget.getArea()); } /** * @return true the mouse is anywhere above the widget */ - public boolean isMouseAbove(IGuiElement widget) { + public boolean isMouseAbove(IWidget widget) { return isMouseAbove(widget.getArea()); } diff --git a/src/main/java/com/cleanroommc/modularui/screen/viewport/ModularGuiContext.java b/src/main/java/com/cleanroommc/modularui/screen/viewport/ModularGuiContext.java index 943e0ce58..86a910083 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/viewport/ModularGuiContext.java +++ b/src/main/java/com/cleanroommc/modularui/screen/viewport/ModularGuiContext.java @@ -5,7 +5,6 @@ import com.cleanroommc.modularui.api.MCHelper; import com.cleanroommc.modularui.api.widget.IDraggable; import com.cleanroommc.modularui.api.widget.IFocusedWidget; -import com.cleanroommc.modularui.api.widget.IGuiElement; import com.cleanroommc.modularui.api.widget.IVanillaSlot; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.api.widget.ResizeDragArea; @@ -107,7 +106,7 @@ public boolean isHovered() { */ @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") @Deprecated - public boolean isHovered(IGuiElement guiElement) { + public boolean isHovered(IWidget guiElement) { return guiElement.isHovering(); } @@ -120,7 +119,7 @@ public boolean isHovered(IGuiElement guiElement) { */ @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") @Deprecated - public boolean isHoveredFor(IGuiElement guiElement, int ticks) { + public boolean isHoveredFor(IWidget guiElement, int ticks) { return guiElement.isHoveringFor(ticks); } @@ -142,9 +141,9 @@ public boolean isHoveredFor(IGuiElement guiElement, int ticks) { } /** - * @return all widgets which are below the mouse ({@link GuiContext#isAbove(IGuiElement)} is true) + * @return all widgets which are below the mouse ({@link GuiContext#isAbove(IWidget)} is true) */ - public Iterable getAllBelowMouse() { + public Iterable getAllBelowMouse() { return this.hoveredWidgets; } @@ -332,7 +331,7 @@ public boolean onHoveredClick(int button, LocatedWidget hovered) { draggable = new LocatedElement<>(iDraggable, hovered.getTransformationMatrix()); } else if (widget instanceof ModularPanel panel) { if (panel.isDraggable()) { - if (!panel.flex().hasFixedSize()) { + if (!panel.resizer().hasFixedSize()) { throw new IllegalStateException("Panel must have a fixed size. It can't specify left AND right or top AND bottom!"); } draggable = new LocatedElement<>(new DraggablePanelWrapper(panel), TransformationMatrix.EMPTY); @@ -488,7 +487,7 @@ public void setSettings(UISettings settings) { } } - private static class HoveredIterable implements Iterable { + private static class HoveredIterable implements Iterable { private final PanelManager panelManager; @@ -498,7 +497,7 @@ private HoveredIterable(PanelManager panelManager) { @NotNull @Override - public Iterator iterator() { + public Iterator iterator() { return new Iterator<>() { private final Iterator panelIt = HoveredIterable.this.panelManager.getOpenPanels().iterator(); @@ -516,7 +515,7 @@ public boolean hasNext() { } @Override - public IGuiElement next() { + public IWidget next() { if (this.widgetIt == null || !this.widgetIt.hasNext()) { this.widgetIt = this.panelIt.next().getHovering().iterator(); } diff --git a/src/main/java/com/cleanroommc/modularui/test/EventHandler.java b/src/main/java/com/cleanroommc/modularui/test/EventHandler.java index a59c251b1..dbd7b1636 100644 --- a/src/main/java/com/cleanroommc/modularui/test/EventHandler.java +++ b/src/main/java/com/cleanroommc/modularui/test/EventHandler.java @@ -1,5 +1,7 @@ package com.cleanroommc.modularui.test; +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.ModularUIConfig; import com.cleanroommc.modularui.api.IThemeApi; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.drawable.IIcon; @@ -8,21 +10,34 @@ import com.cleanroommc.modularui.drawable.GuiTextures; import com.cleanroommc.modularui.factory.ClientGUI; import com.cleanroommc.modularui.holoui.HoloUI; +import com.cleanroommc.modularui.screen.CustomModularScreen; +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.ModularScreen; +import com.cleanroommc.modularui.screen.OpenScreenEvent; import com.cleanroommc.modularui.screen.RichTooltipEvent; import com.cleanroommc.modularui.screen.viewport.GuiContext; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.ReloadThemeEvent; import com.cleanroommc.modularui.theme.SelectableTheme; import com.cleanroommc.modularui.theme.ThemeBuilder; import com.cleanroommc.modularui.theme.WidgetTheme; import com.cleanroommc.modularui.utils.Color; import com.cleanroommc.modularui.utils.Platform; +import com.cleanroommc.modularui.widgets.ButtonWidget; +import com.cleanroommc.modularui.widgets.TextWidget; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.util.text.TextFormatting; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicInteger; + public class EventHandler { public static boolean enabledRichTooltipEventTest = false; @@ -81,4 +96,60 @@ public void onRichTooltip(RichTooltipEvent.Pre event) { public void onThemeReload(ReloadThemeEvent.Pre event) { IThemeApi.get().registerTheme(testTheme); } + + @SubscribeEvent + public void onOpenScreen(OpenScreenEvent event) { + if (ModularUIConfig.enableTestOverlays) { + if (event.getScreen() instanceof GuiMainMenu gui) { + event.addOverlay(getMainMenuOverlayTest(gui)); + } else if (event.getScreen() instanceof GuiContainer gui) { + event.addOverlay(getContainerOverlayTest(gui)); + } + } + } + + private ModularScreen getMainMenuOverlayTest(GuiMainMenu gui) { + TextWidget title = new TextWidget<>(IKey.str("ModularUI")); + int[] colors = {Color.WHITE.main, Color.AMBER.main, Color.BLUE.main, Color.GREEN.main, Color.DEEP_PURPLE.main, Color.RED.main}; + AtomicInteger k = new AtomicInteger(); + return new ModularScreen(ModularUI.ID, + ModularPanel.defaultPanel("overlay") + .fullScreenInvisible() + .child(title.scale(5f) + .shadow(true) + .color(colors[k.get()]) + .leftRel(0.5f).topRel(0.07f)) + .child(new ButtonWidget<>() // test button overlapping + .topRel(0.25f, 59, 0f) + .leftRelOffset(0.5f, 91) + .size(44) + .overlay(IKey.str("Fun Button")) + .onMousePressed(mouseButton -> { + k.set((k.get() + 1) % colors.length); + title.color(colors[k.get()]); + return true; + }))); + } + + private ModularScreen getContainerOverlayTest(GuiContainer gui) { + return new CustomModularScreen(ModularUI.ID) { + + @Override + public @NotNull ModularPanel buildUI(ModularGuiContext context) { + return ModularPanel.defaultPanel("watermark_overlay", gui.getXSize(), gui.getYSize()) + .pos(gui.getGuiLeft(), gui.getGuiTop()) + .invisible() + .child(GuiTextures.MUI_LOGO.asIcon().asWidget() + .top(5).right(5) + .size(18)); + } + + @Override + public void onResize(int width, int height) { + getMainPanel().pos(gui.getGuiLeft(), gui.getGuiTop()) + .size(gui.getXSize(), gui.getYSize()); + super.onResize(width, height); + } + }; + } } diff --git a/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java b/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java index 8797fef4c..f2ea42dce 100644 --- a/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java +++ b/src/main/java/com/cleanroommc/modularui/test/GLTestGui.java @@ -1,5 +1,6 @@ package com.cleanroommc.modularui.test; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.drawable.IKey; import com.cleanroommc.modularui.drawable.GuiTextures; @@ -40,6 +41,10 @@ public class GLTestGui extends CustomModularScreen { private RenderObject ro1; private RenderObject ro2; + public GLTestGui() { + super(ModularUI.ID); + } + @Override public @NotNull ModularPanel buildUI(ModularGuiContext context) { this.ro1 = new RenderObject(); diff --git a/src/main/java/com/cleanroommc/modularui/test/ItemEditorGui.java b/src/main/java/com/cleanroommc/modularui/test/ItemEditorGui.java index 2687e5cb4..fc6b99f00 100644 --- a/src/main/java/com/cleanroommc/modularui/test/ItemEditorGui.java +++ b/src/main/java/com/cleanroommc/modularui/test/ItemEditorGui.java @@ -1,18 +1,20 @@ package com.cleanroommc.modularui.test; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.IGuiHolder; import com.cleanroommc.modularui.api.drawable.IKey; import com.cleanroommc.modularui.factory.GuiData; import com.cleanroommc.modularui.factory.SimpleGuiFactory; import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.screen.UISettings; import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.value.sync.IntSyncValue; import com.cleanroommc.modularui.value.sync.PanelSyncManager; import com.cleanroommc.modularui.value.sync.StringSyncValue; -import com.cleanroommc.modularui.widgets.slot.ItemSlot; import com.cleanroommc.modularui.widgets.layout.Column; import com.cleanroommc.modularui.widgets.layout.Row; +import com.cleanroommc.modularui.widgets.slot.ItemSlot; import com.cleanroommc.modularui.widgets.slot.ModularSlot; import com.cleanroommc.modularui.widgets.textfield.TextFieldWidget; @@ -43,6 +45,11 @@ private void setStack(ItemStack stack) { this.stackHandler.setStackInSlot(0, stack); } + @Override + public ModularScreen createScreen(GuiData data, ModularPanel mainPanel) { + return new ModularScreen(ModularUI.ID, mainPanel); + } + @Override public ModularPanel buildUI(GuiData data, PanelSyncManager syncManager, UISettings settings) { ItemStack itemStack = data.getPlayer().getHeldItemMainhand(); diff --git a/src/main/java/com/cleanroommc/modularui/test/OverlayTest.java b/src/main/java/com/cleanroommc/modularui/test/OverlayTest.java deleted file mode 100644 index 7adcbb56e..000000000 --- a/src/main/java/com/cleanroommc/modularui/test/OverlayTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.cleanroommc.modularui.test; - -import com.cleanroommc.modularui.api.drawable.IKey; -import com.cleanroommc.modularui.drawable.GuiTextures; -import com.cleanroommc.modularui.overlay.OverlayHandler; -import com.cleanroommc.modularui.overlay.OverlayManager; -import com.cleanroommc.modularui.screen.CustomModularScreen; -import com.cleanroommc.modularui.screen.ModularPanel; -import com.cleanroommc.modularui.screen.ModularScreen; -import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; -import com.cleanroommc.modularui.utils.Color; -import com.cleanroommc.modularui.widgets.ButtonWidget; -import com.cleanroommc.modularui.widgets.TextWidget; - -import net.minecraft.client.gui.GuiMainMenu; -import net.minecraft.client.gui.inventory.GuiContainer; - -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.atomic.AtomicInteger; - -public class OverlayTest { - - public static void init() { - - OverlayManager.register(new OverlayHandler(screen -> screen instanceof GuiMainMenu, screen -> { - GuiMainMenu gui = (GuiMainMenu) screen; - TextWidget title = new TextWidget<>(IKey.str("ModularUI")); - int[] colors = {Color.WHITE.main, Color.AMBER.main, Color.BLUE.main, Color.GREEN.main, Color.DEEP_PURPLE.main, Color.RED.main}; - AtomicInteger k = new AtomicInteger(); - return new ModularScreen(ModularPanel.defaultPanel("overlay") - .fullScreenInvisible() - .child(title.scale(5f) - .shadow(true) - .color(colors[k.get()]) - .leftRel(0.5f).topRel(0.07f)) - .child(new ButtonWidget<>() // test button overlapping - .topRel(0.25f, 59, 0f) - .leftRelOffset(0.5f, 91) - .size(44) - .overlay(IKey.str("Fun Button")) - .onMousePressed(mouseButton -> { - k.set((k.get() + 1) % colors.length); - title.color(colors[k.get()]); - return true; - }))); - })); - - OverlayManager.register(new OverlayHandler(screen -> screen instanceof GuiContainer, screen -> { - GuiContainer gui = (GuiContainer) screen; - return new CustomModularScreen() { - - @Override - public @NotNull ModularPanel buildUI(ModularGuiContext context) { - return ModularPanel.defaultPanel("watermark_overlay", gui.getXSize(), gui.getYSize()) - .pos(gui.getGuiLeft(), gui.getGuiTop()) - .invisible() - .child(GuiTextures.MUI_LOGO.asIcon().asWidget() - .top(5).right(5) - .size(18)); - } - - @Override - public void onResize(int width, int height) { - getMainPanel().pos(gui.getGuiLeft(), gui.getGuiTop()) - .size(gui.getXSize(), gui.getYSize()); - super.onResize(width, height); - } - }; - })); - } -} diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGui.java b/src/main/java/com/cleanroommc/modularui/test/TestGui.java index d7c6d64bd..ae48628d7 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGui.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGui.java @@ -35,6 +35,10 @@ public class TestGui extends CustomModularScreen { private Map availableElements; + public TestGui() { + super(ModularUI.ID); + } + @Override public void onClose() { ModularUI.LOGGER.info("New values: {}", this.configuredOptions); @@ -50,7 +54,8 @@ public void onClose() { final Map> items = new Object2ObjectOpenHashMap<>(); for (String line : this.lines) { items.put(line, new SortableListWidget.Item<>(line) - .child(item -> new Row() + .name("item_" + line) + .child(item -> new Row().name("row_" + line) .child(new Widget<>() .addTooltipLine(line) .widgetTheme(IThemeApi.BUTTON) diff --git a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java index c2c33dbc7..80f86870b 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestGuis.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestGuis.java @@ -39,6 +39,7 @@ import com.cleanroommc.modularui.utils.fakeworld.ISchema; import com.cleanroommc.modularui.value.BoolValue; import com.cleanroommc.modularui.value.IntValue; +import com.cleanroommc.modularui.value.ObjectValue; import com.cleanroommc.modularui.value.StringValue; import com.cleanroommc.modularui.widget.DraggableWidget; import com.cleanroommc.modularui.widget.Widget; @@ -54,6 +55,8 @@ import com.cleanroommc.modularui.widgets.layout.Flow; import com.cleanroommc.modularui.widgets.layout.Grid; import com.cleanroommc.modularui.widgets.layout.Row; +import com.cleanroommc.modularui.widgets.menu.ContextMenuButton; +import com.cleanroommc.modularui.widgets.menu.DropdownWidget; import com.cleanroommc.modularui.widgets.textfield.TextFieldWidget; import net.minecraft.client.Minecraft; @@ -79,11 +82,17 @@ import java.util.Comparator; import java.util.List; import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; public class TestGuis extends CustomModularScreen { public static boolean withCode = false; + public TestGuis() { + super(ModularUI.ID); + } + @Override public @NotNull ModularPanel buildUI(ModularGuiContext context) { // collect all test from all build methods in this class via reflection @@ -122,7 +131,7 @@ public class TestGuis extends CustomModularScreen { .leftRel(1f) .heightRel(1f)); } - ClientGUI.open(new ModularScreen(panel).openParentOnClose(true)); + ClientGUI.open(new ModularScreen(ModularUI.ID, panel).openParentOnClose(true)); } catch (IllegalAccessException | InvocationTargetException e) { ModularUI.LOGGER.throwing(e); } @@ -194,8 +203,7 @@ public void onInit() { animator.reset(true); animator.animate(true); return ModularPanel.defaultPanel("main").size(150) - .child(new TransformWidget() - .child(widget) + .child(new TransformWidget(widget) .transform(stack -> { double angle = Math.PI; float x = (float) (55 * Math.cos(animator.getValue() * angle)); @@ -279,6 +287,7 @@ public void onInit() { @Override public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { GuiDraw.drawEntity(entity, 0, 0, width, height, context.getCurrentDrawingZ(), e -> { + // TODO the drawable doesnt seem to update the rotation float scale = 0.9f; GlStateManager.scale(scale, scale, scale); GlStateManager.translate(0, 7, 0); @@ -525,7 +534,7 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { } public static @NotNull ModularPanel buildViewportTransformUI() { - return new TestPanel("test") + return new TestPanel("viewport_transform") .child(new Widget<>() .align(Alignment.Center) .size(50, 50) @@ -533,6 +542,53 @@ public static ModularPanel buildCollapseDisabledChildrenUI() { .hoverBackground(GuiTextures.MC_BUTTON_HOVERED)); } + public static ModularPanel buildContextMenu() { + List options1 = IntStream.range(0, 5).mapToObj(i -> "Option " + (i + 1)).collect(Collectors.toList()); + List options2 = IntStream.range(0, 5).mapToObj(i -> "Sub Option " + (i + 1)).collect(Collectors.toList()); + ObjectValue itemValue = new ObjectValue<>(ItemStack.class, new ItemStack(Items.ACACIA_DOOR)); + return new ModularPanel("context_menu_test") + .size(150) + .child(new ContextMenuButton<>("menu") + .top(7) + .width(100) + .horizontalCenter() + .height(16) + .overlay(IKey.str("Menu")) + .menuList(l -> l + .maxSize(80) + .children(options1, s -> IKey.str(s).asWidget()) + .child(new ContextMenuButton<>("sub_menu") + .widthRel(1f) + .height(12) + .overlay(IKey.str("Sub Menu")) + .openRightDown() + .menuList(l1 -> l1 + //.width(90) + .maxSize(80) + .children(options2, s -> IKey.str(s).asWidget()))))) + .child(new DropdownWidget<>("test_dropdown", ItemStack.class) + .top(45) + .width(100) + .horizontalCenter() + .value(itemValue) + .option(new ItemStack(Items.ACACIA_DOOR)) + .option(new ItemStack(Items.GOLD_INGOT)) + .option(new ItemStack(Items.APPLE)) + .option(new ItemStack(Items.FURNACE_MINECART)) + .option(new ItemStack(Items.IRON_SHOVEL)) + .option(new ItemStack(Items.STICK)) + .option(new ItemStack(Items.NETHER_STAR)) + .optionToWidget((item, forSelected) -> Flow.row() + .coverChildrenHeight() + .padding(4, 1) + .mainAxisAlignment(Alignment.MainAxis.SPACE_BETWEEN) + .child(new ItemDrawable(item).asWidget()) + .child(IKey.str(item.getDisplayName()).asWidget() + .widgetTheme(IThemeApi.BUTTON) + .invisible())) + ); + } + public static @NotNull ModularPanel buildGraphUI() { double[] x = DAM.linspace(-25, 25, 200); // sin(x) / x diff --git a/src/main/java/com/cleanroommc/modularui/test/TestItem.java b/src/main/java/com/cleanroommc/modularui/test/TestItem.java index 90417fdcc..152ea00aa 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestItem.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestItem.java @@ -7,6 +7,7 @@ import com.cleanroommc.modularui.factory.PlayerInventoryGuiData; import com.cleanroommc.modularui.factory.inventory.InventoryTypes; import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.screen.UISettings; import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.utils.ItemCapabilityProvider; @@ -49,6 +50,11 @@ public class TestItem extends Item implements IGuiHolder public static final TestItem testItem = new TestItem(); + @Override + public ModularScreen createScreen(PlayerInventoryGuiData data, ModularPanel mainPanel) { + return new ModularScreen(ModularUI.ID, mainPanel); + } + @Override public ModularPanel buildUI(PlayerInventoryGuiData guiData, PanelSyncManager guiSyncManager, UISettings settings) { IItemHandlerModifiable itemHandler = (IItemHandlerModifiable) guiData.getUsedItemStack().getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, null); diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index c990003e7..dfc1f22f2 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -179,7 +179,8 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI return new Column() .widthRel(1f) .coverChildrenHeight() - .children(vals.size(), i -> IKey.str(String.valueOf(vals.get(i))).asWidget().padding(2)); + .children(vals.size(), i -> IKey.str(String.valueOf(vals.get(i))).asWidget().padding(2)) + .name("synced number col"); }); Rectangle colorPickerBackground = new Rectangle().color(Color.RED.main); @@ -191,7 +192,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .top(0) .rightRel(1f), true); PagedWidget.Controller tabController = new PagedWidget.Controller(); - panel.flex() // returns object which is responsible for sizing + panel.resizer() // returns object which is responsible for sizing .size(176, 220) // set a static size for the main panel .align(Alignment.Center); // center the panel in the screen panel @@ -239,6 +240,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .build() .margin(5, 5, 20, 5).name("crafting")))) .child(Flow.column() + .name("main col") .sizeRel(1f) .paddingBottom(7) .child(new ParentWidget<>() @@ -476,21 +478,22 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UI .name("page 4 storage") .sizeRel(1f) .child(new Column() - .padding(7) - .child(new ItemSlot() - .slot(new ModularSlot(this.storageInventory0, 0) - .changeListener(((newItem, onlyAmountChanged, client, init) -> { - if (client && !onlyAmountChanged) { - dynamicSyncHandler.notifyUpdate(packet -> NetworkUtils.writeItemStack(packet, newItem)); - } - })))) - .child(new DynamicSyncedWidget<>() - .widthRel(1f) - .syncHandler(dynamicSyncHandler)) - .child(new DynamicSyncedWidget<>() + .name("page 4 col, dynamic widgets") + .padding(7) + .child(new ItemSlot() + .slot(new ModularSlot(this.storageInventory0, 0) + .changeListener(((newItem, onlyAmountChanged, client, init) -> { + if (client && !onlyAmountChanged) { + dynamicSyncHandler.notifyUpdate(packet -> NetworkUtils.writeItemStack(packet, newItem)); + } + })))) + .child(new DynamicSyncedWidget<>() + .widthRel(1f) + .syncHandler(dynamicSyncHandler)) + /*.child(new DynamicSyncedWidget<>() .widthRel(1f) .coverChildrenHeight() - .syncHandler(dynamicLinkedSyncHandler))) + .syncHandler(dynamicLinkedSyncHandler))*/) ) .addPage(createSchemaPage(guiData)))) .child(SlotGroupWidget.playerInventory(false)) @@ -591,10 +594,10 @@ public void buildDialog(Dialog dialog) { AtomicReference value = new AtomicReference<>(""); dialog.setDraggable(true); dialog.child(new TextFieldWidget() - .flex(flex -> flex.size(100, 20).align(Alignment.Center)) + .resizer(flex -> flex.size(100, 20).align(Alignment.Center)) .value(new StringValue.Dynamic(value::get, value::set))) .child(new ButtonWidget<>() - .flex(flex -> flex.size(8, 8).top(5).right(5)) + .resizer(flex -> flex.size(8, 8).top(5).right(5)) .overlay(IKey.str("x")) .onMousePressed(mouseButton -> { dialog.closeWith(value.get()); diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile2.java b/src/main/java/com/cleanroommc/modularui/test/TestTile2.java index 1850f62eb..0a3f72b32 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile2.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile2.java @@ -1,11 +1,13 @@ package com.cleanroommc.modularui.test; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.IGuiHolder; import com.cleanroommc.modularui.api.IPanelHandler; import com.cleanroommc.modularui.drawable.GuiTextures; import com.cleanroommc.modularui.drawable.ItemDrawable; import com.cleanroommc.modularui.factory.PosGuiData; import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.screen.UISettings; import com.cleanroommc.modularui.value.sync.PanelSyncManager; import com.cleanroommc.modularui.widget.ScrollWidget; @@ -43,6 +45,11 @@ public TestTile2() { } } + @Override + public ModularScreen createScreen(PosGuiData data, ModularPanel mainPanel) { + return new ModularScreen(ModularUI.ID, mainPanel); + } + @Override public ModularPanel buildUI(PosGuiData data, PanelSyncManager syncManager, UISettings settings) { ScrollWidget sw = new ScrollWidget<>(new VerticalScrollData()).size(9 * 18 + 4, 9 * 18).margin(7).top(20); diff --git a/src/main/java/com/cleanroommc/modularui/theme/ThemeAPI.java b/src/main/java/com/cleanroommc/modularui/theme/ThemeAPI.java index 8b3d778d2..14c25ca73 100644 --- a/src/main/java/com/cleanroommc/modularui/theme/ThemeAPI.java +++ b/src/main/java/com/cleanroommc/modularui/theme/ThemeAPI.java @@ -61,19 +61,31 @@ public List getJavaDefaultThemes(String id) { } @Override - public ITheme getThemeForScreen(String owner, String name, @Nullable String defaultTheme) { - String theme = getThemeIdForScreen(owner, name); + public ITheme getThemeForScreen(String owner, String name, @Nullable String panel, @Nullable String defaultTheme, @Nullable String fallbackTheme) { + String theme = getThemeIdForScreen(owner, name, panel); if (theme != null) return getTheme(theme); if (defaultTheme != null) return getTheme(defaultTheme); + if (fallbackTheme != null) return getTheme(fallbackTheme); return getTheme(ModularUIConfig.useDarkThemeByDefault ? "vanilla_dark" : "vanilla"); } - private String getThemeIdForScreen(String mod, String name) { + private String getThemeIdForScreen(String mod, String name, String panelName) { String fullName = mod + ":" + name; - String theme = this.jsonScreenThemes.get(fullName); + String fullPanelName = null; + if (panelName != null) fullPanelName = fullName + ":" + panelName; + String theme = null; + if (fullPanelName != null) { + theme = this.jsonScreenThemes.get(fullPanelName); + if (theme != null) return theme; + } + theme = this.jsonScreenThemes.get(fullName); if (theme != null) return theme; theme = this.jsonScreenThemes.get(mod); if (theme != null) return theme; + if (fullPanelName != null) { + theme = this.screenThemes.get(fullPanelName); + if (theme != null) return theme; + } theme = this.screenThemes.get(fullName); return theme != null ? theme : this.screenThemes.get(mod); } diff --git a/src/main/java/com/cleanroommc/modularui/utils/MutableSingletonList.java b/src/main/java/com/cleanroommc/modularui/utils/MutableSingletonList.java new file mode 100644 index 000000000..111b543db --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/MutableSingletonList.java @@ -0,0 +1,285 @@ +package com.cleanroommc.modularui.utils; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +public class MutableSingletonList implements List { + + private boolean hasValue; + private T value; + + public MutableSingletonList() { + remove(); + } + + public MutableSingletonList(T value) { + set(value); + } + + public T get() { + if (!this.hasValue) throw new IndexOutOfBoundsException("List is empty but tried to access index 0!"); + return value; + } + + public T getOrNull() { + return hasValue ? value : null; + } + + public void set(T t) { + this.value = t; + this.hasValue = true; + } + + public void remove() { + this.value = null; + this.hasValue = false; + } + + @Override + public int size() { + return hasValue ? 1 : 0; + } + + @Override + public boolean isEmpty() { + return !hasValue; + } + + public boolean hasValue() { + return hasValue; + } + + @Override + public boolean contains(Object o) { + return this.hasValue && Objects.equals(this.value, o); + } + + @Override + public @NotNull Iterator iterator() { + return new Iterator<>() { + + private byte cursor = 0; + + @Override + public boolean hasNext() { + return MutableSingletonList.this.hasValue && this.cursor == 0; + } + + @Override + public T next() { + if (!hasNext()) throw new NoSuchElementException(); + this.cursor++; + return MutableSingletonList.this.value; + } + + @Override + public void remove() { + if (this.cursor < 1) throw new IllegalStateException(); + MutableSingletonList.this.remove(); + this.cursor--; + } + }; + } + + @Override + public @NotNull Object @NotNull [] toArray() { + if (!this.hasValue) return new Object[0]; + Object[] o = new Object[1]; + o[0] = this.value; + return o; + } + + @Override + public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { + if (!this.hasValue) return a; + if (a.length == 0) a = Arrays.copyOf(a, 1); + a[0] = (T1) this.value; + return a; + } + + @Override + public boolean add(T t) { + if (this.hasValue) throw new IllegalStateException("MutableSingletonList can only have one value, but it already has a value!"); + set(t); + return false; + } + + @Override + public boolean remove(Object o) { + if (contains(o)) { + remove(); + return true; + } + return false; + } + + @Override + public boolean containsAll(@NotNull Collection c) { + int s = c.size(); + if (s > 1 || (s == 1 != this.hasValue)) return false; + if (!this.hasValue) return true; + if (c instanceof List l) return Objects.equals(this.value, l.get(0)); + return Objects.equals(this.value, c.iterator().next()); + } + + @Override + public boolean addAll(@NotNull Collection c) { + if (this.hasValue || c.isEmpty()) return false; + if (c instanceof List l) { + add((T) l.get(0)); + } else { + add(c.iterator().next()); + } + return true; + } + + private void verifyIndex(int i, boolean checkEmpty) { + if (i != 0) throw new IndexOutOfBoundsException("MutableSingletonList only accepts index 0!"); + if (checkEmpty && !this.hasValue) + throw new IndexOutOfBoundsException("Tried to access index 0, but MutableSingletonList has no element!"); + } + + @Override + public boolean addAll(int index, @NotNull Collection c) { + verifyIndex(index, false); + return addAll(c); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + if (!this.hasValue || c.isEmpty()) return false; + if (c instanceof List l) { + return remove(l.get(0)); + } + return remove(c.iterator().next()); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + if (!this.hasValue || c.contains(this.value)) return false; + remove(); + return true; + } + + @Override + public void clear() { + remove(); + } + + @Override + public T get(int index) { + verifyIndex(index, true); + return null; + } + + @Override + public T set(int index, T element) { + verifyIndex(index, true); + T t = this.value; + this.value = element; + return t; + } + + @Override + public void add(int index, T element) { + verifyIndex(index, false); + add(element); + } + + @Override + public T remove(int index) { + verifyIndex(index, true); + T t = this.value; + remove(); + return t; + } + + @Override + public int indexOf(Object o) { + return contains(o) ? 0 : -1; + } + + @Override + public int lastIndexOf(Object o) { + return indexOf(o); + } + + @Override + public @NotNull ListIterator listIterator() { + return new ListIterator() { + + private byte cursor = 0; + + @Override + public boolean hasNext() { + return MutableSingletonList.this.hasValue && (this.cursor == 0 || this.cursor == -1); + } + + @Override + public T next() { + if (!hasNext()) throw new NoSuchElementException(); + this.cursor = 1; + return MutableSingletonList.this.value; + } + + @Override + public boolean hasPrevious() { + return MutableSingletonList.this.hasValue && (this.cursor == 0 || this.cursor == 1); + } + + @Override + public T previous() { + if (!hasNext()) throw new NoSuchElementException(); + this.cursor = -1; + return MutableSingletonList.this.value; + } + + @Override + public int nextIndex() { + return cursor == 0 ? 0 : 1; + } + + @Override + public int previousIndex() { + return cursor == 0 ? 0 : -1; + } + + @Override + public void remove() { + if (this.cursor == 0) throw new IllegalStateException(); + MutableSingletonList.this.remove(); + this.cursor = 0; + } + + @Override + public void set(T t) { + if (this.cursor == 0) throw new IllegalStateException(); + MutableSingletonList.this.set(t); + } + + @Override + public void add(T t) { + MutableSingletonList.this.add(t); + } + }; + } + + @Override + public @NotNull ListIterator listIterator(int index) { + verifyIndex(index, false); + return listIterator(); + } + + @Override + public @NotNull List subList(int fromIndex, int toIndex) { + if (fromIndex < 0 || toIndex > 1 || toIndex < fromIndex) throw new IndexOutOfBoundsException(); + return new MutableSingletonList<>(this.value); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/utils/TreeUtil.java b/src/main/java/com/cleanroommc/modularui/utils/TreeUtil.java new file mode 100644 index 000000000..8fd019312 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/utils/TreeUtil.java @@ -0,0 +1,502 @@ +package com.cleanroommc.modularui.utils; + +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.api.ITreeNode; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.widget.sizer.ResizeNode; + +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.Streams; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public class TreeUtil { + + public static boolean allowUnicode = true; + + private static final String U_T = "✓"; + private static final String U_F = "✘"; + private static final String T = "T"; + private static final String F = "F"; + private static final String U_PIPE = "│"; + private static final String U_PIPE_MID = "├"; + private static final String U_PIPE_END = "└"; + private static final String PIPE = "|"; + private static final String PIPE_MID = "+"; + private static final String PIPE_END = "-"; + + public static final NodeInfo RESIZE_NODE_INFO_FULLY_RESIZED = (root, node, builder) -> builder + .append("Fully resized: ") + .append(str(node.isFullyCalculated(node.hasParent() && node.getParent().isLayout()))); + public static final NodeInfo RESIZE_NODE_INFO_RESIZED_DETAILED = (root, node, builder) -> { + builder.append("XYWH: ") + .append(str(node.isXCalculated())) + .append(str(node.isYCalculated())) + .append(str(node.isWidthCalculated())) + .append(str(node.isHeightCalculated())) + .append(", Children resized: ").append(str(node.areChildrenCalculated())) + .append(", Self layout done: ").append(str(node.isLayoutDone())) + .append(", Parent layout done: ").append(str(!node.canRelayout(node.hasParent() && node.getParent().isLayout()))); + /*if (!self) { + builder.append(", Self detail: ("); + RESIZE_NODE_INFO_SELF_RESIZED_DETAIL.addInfo(root, node, builder); + builder.append(")"); + }*/ + }; + public static final NodeInfo RESIZE_NODE_INFO_RESIZED_COLLAPSED = (root, node, builder) -> { + if (node.isFullyCalculated(node.hasParent() && node.getParent().isLayout())) { + RESIZE_NODE_INFO_FULLY_RESIZED.addInfo(root, node, builder); + } else { + RESIZE_NODE_INFO_RESIZED_DETAILED.addInfo(root, node, builder); + } + }; + + private static String str(boolean b) { + if (TreeUtil.allowUnicode) return b ? U_T : U_F; + return b ? T : F; + } + + public static > boolean foreachChildBFS(T parent, Predicate consumer) { + return foreachChildBFS(parent, consumer, false); + } + + /** + * Iterates through the whole sub widget tree by using breath-first-search. + *

+ * This method delivers good performance and can outperform {@link #foreachChild(T, Predicate, boolean)} in certain small widget + * trees. + * + * @param parent starting point + * @param consumer Operation on each child. Return false to terminate the iteration. + * @param includeSelf true if the consumer should also consume the parent + * @return true if the iteration was not terminated by the consumer + */ + public static > boolean foreachChildBFS(T parent, Predicate consumer, boolean includeSelf) { + if (includeSelf && !consumer.test(parent)) return false; + ObjectList parents = ObjectList.create(); + parents.add(parent); + while (!parents.isEmpty()) { + for (T child : parents.removeFirst().getChildren()) { + if (child.hasChildren()) { + parents.addLast(child); + } + if (!consumer.test(child)) return false; + } + } + return true; + } + + /** + * @see #foreachChild(T, Predicate, boolean) + */ + public static > boolean foreachChild(T parent, Predicate consumer) { + return foreachChild(parent, consumer, false); + } + + /** + * Iterates through the whole sub widget tree recursively. + *

+ * This method has the best performance in most cases, but can be outperformed on certain small widget trees. + * + * @param parent starting point + * @param consumer Operation on each child. Return false to terminate the iteration. + * @param includeSelf true if the consumer should also consume the parent + * @return true if the iteration was not terminated by the consumer + */ + public static > boolean foreachChild(T parent, Predicate consumer, boolean includeSelf) { + if (includeSelf && !consumer.test(parent)) return false; + if (!parent.hasChildren()) return true; + for (T widget : parent.getChildren()) { + if (!consumer.test(widget)) return false; + if (widget.hasChildren() && !foreachChild(widget, consumer, false)) { + return false; + } + } + return true; + } + + /** + * Iterates through the whole sub widget tree recursively. Unlike {@link #foreachChild(T, Predicate, boolean)}, which can only + * return a boolean, this method can return any type. Once the consumer returns a non-null value, the iteration is terminated and the + * value will be returned. + * + * @param parent starting point + * @param consumer Operation on each child. Return a non-null value to terminate the iteration and to return the value. + * @param includeSelf true if the consumer should also consume the parent + * @return the first resulting value of the consumer or null of it always returned null + */ + public static , V> @Nullable V foreachChildWithResult(T parent, Function consumer, boolean includeSelf) { + if (includeSelf) { + V t = consumer.apply(parent); + if (t != null) return t; + } + if (!parent.hasChildren()) return null; + for (T widget : parent.getChildren()) { + V t = consumer.apply(widget); + if (t != null) return t; + if (widget.hasChildren()) { + t = foreachChildWithResult(widget, consumer, false); + if (t != null) return t; + } + } + return null; + } + + public static > boolean foreachChildReverse(T parent, Predicate consumer, boolean includeSelf) { + if (parent.getChildren().isEmpty()) { + return !includeSelf || consumer.test(parent); + } + for (T widget : parent.getChildren()) { + if (!widget.getChildren().isEmpty() && foreachChildReverse(widget, consumer, false)) { + return false; + } + if (!consumer.test(widget)) return false; + } + return !includeSelf || consumer.test(parent); + } + + /** + * Creates a flat stream of the whole sub widget tree. + *

+ * {@link Stream#forEach(Consumer)} on this has slightly worse performance than {@link #foreachChildBFS(T, Predicate, boolean)} on + * small widget trees and has similar performance on large widget trees. The performance is significantly better than + * {@link #iteratorBFS(T)} even though this method uses it. + * + * @param parent starting point. + * @return stream of the sub widget tree + */ + @SuppressWarnings("UnstableApiUsage") + public static > Stream flatStreamBFS(T parent) { + if (!parent.hasChildren()) return Stream.of(parent); + return Streams.stream(iteratorBFS(parent)); + } + + public static > @UnmodifiableView Iterable iterableBFS(T parent) { + return () -> iteratorBFS(parent); + } + + /** + * Creates an unmodifiable iterator of the whole sub widget tree. + *

+ * This method of iterating has the worst performance in every case. It's roughly 4 times worse than + * {@link #foreachChildBFS(T, Predicate, boolean)}. If not used extensively the performance is still nothing to worry about. + * + * @param parent starting point + * @return an unmodifiable iterator of the sub widget tree + */ + public static > @UnmodifiableView Iterator iteratorBFS(T parent) { + return new AbstractIterator<>() { + + private final ObjectList queue = ObjectList.create(); + private Iterator currentIt; + + @Override + protected T computeNext() { + if (currentIt == null) { + currentIt = parent.getChildren().iterator(); + return parent; + } + if (currentIt.hasNext()) return handleWidget(currentIt.next()); + while (!queue.isEmpty()) { + currentIt = queue.removeFirst().getChildren().iterator(); + if (currentIt.hasNext()) return handleWidget(currentIt.next()); + } + return endOfData(); + } + + private T handleWidget(T widget) { + if (widget.hasChildren()) { + queue.addLast(widget); + } + return widget; + } + }; + } + + /** + * Finds all children in the sub widget tree which match the test and puts them in a list. + * + * @param parent starting point + * @param test test which the target children have to pass + * @return a list of matching children + */ + public static > List flatList(T parent, Predicate test) { + List widgets = new ArrayList<>(); + foreachChild(parent, w -> { + if (test.test(w)) widgets.add(w); + return true; + }, true); + return widgets; + } + + /** + * Finds all children in the sub widget tree which match the test and puts them in a list. + * + * @param parent starting point + * @param test test which the target children have to pass + * @return a list of matching children + */ + public static > List flatListBFS(T parent, Predicate test) { + List widgets = new ArrayList<>(); + foreachChildBFS(parent, w -> { + if (test.test(w)) widgets.add(w); + return true; + }, true); + return widgets; + } + + public static , R extends ITreeNode> List flatListByType(T parent, Class type) { + return flatListByType(parent, type, null); + } + + /** + * Finds all widgets in the sub widget tree which match the given type and additional test. + * + * @param parent starting point + * @param type type of the target widgets + * @param test test which the target widgets have to pass + * @param type of the target widgets + * @return a list of matching widgets + */ + @SuppressWarnings("unchecked") + public static , R extends ITreeNode> List flatListByType(T parent, Class type, @Nullable Predicate test) { + List widgets = new ArrayList<>(); + foreachChild(parent, w -> { + if (type.isAssignableFrom(w.getClass())) { + if (test == null || test.test((R) w)) widgets.add((R) w); + } + return true; + }, true); + return widgets; + } + + /** + * Finds the first widget in the sub widget tree, for which the test returns true. + * + * @param parent starting point + * @param test test which the widget has to pass + * @return the first matching widget + */ + public static > T findFirst(T parent, @NotNull Predicate test) { + return foreachChildWithResult(parent, w -> { + if (test.test(w)) { + return w; + } + return null; + }, true); + } + + /** + * Finds the first widget in the sub widget tree with the given type, for which the test returns true. + * + * @param parent starting point + * @param type type of the target widget + * @param test test which the widget has to pass + * @return the first matching widget + */ + @SuppressWarnings("unchecked") + public static , R extends ITreeNode> R findFirst(T parent, Class type, @Nullable Predicate test) { + return foreachChildWithResult(parent, t -> { + if (type.isAssignableFrom(t.getClass())) { + if (test == null || test.test((R) t)) { + return (R) t; + } + } + return null; + }, true); + } + + public static > T findParent(T parent, Predicate filter) { + if (parent == null) return null; + while (!(parent instanceof ModularPanel)) { + if (filter.test(parent)) { + return parent; + } + parent = parent.getParent(); + } + return filter.test(parent) ? parent : null; + } + + public static , R extends ITreeNode> R findParent(T parent, Class type) { + return findParent(parent, type, null); + } + + @SuppressWarnings("unchecked") + public static , R extends ITreeNode> R findParent(T parent, Class type, @Nullable Predicate test) { + if (parent == null) return null; + while (!(parent instanceof ModularPanel)) { + if (type.isAssignableFrom(parent.getClass()) && (test == null || test.test((R) parent))) { + return (R) parent; + } + parent = parent.getParent(); + } + return type.isAssignableFrom(parent.getClass()) && (test == null || test.test((R) parent)) ? (R) parent : null; + } + + /** + * Prints the whole sub widget tree to the log as a human-readable tree graph with Unicode characters. You may need to enabled Unicode + * characters in your IDE terminal to display them properly. + * + * @param parent starting point + */ + public static > void print(T parent) { + print(parent, w -> true, null); + } + + /** + * Prints the whole sub widget tree to the log as a human-readable tree graph with Unicode characters. You may need to enabled Unicode + * characters in your IDE terminal to display them properly. + * + * @param parent starting point + * @param additionalInfo additional info function which is executed for each widget + */ + public static > void print(T parent, NodeInfo additionalInfo) { + print(parent, w -> true, additionalInfo); + } + + /** + * Prints the whole sub widget tree to the log as a human-readable tree graph with Unicode characters. You may need to enabled Unicode + * characters in your IDE terminal to display them properly. + * + * @param parent starting point + * @param test test widgets have to pass to be added to the string builder + */ + public static > void print(T parent, Predicate test) { + print(parent, test, null); + } + + /** + * Prints the whole sub widget tree to the log as a human-readable tree graph with Unicode characters. You may need to enabled Unicode + * characters in your IDE terminal to display them properly. + * + * @param parent starting point + * @param test test widgets have to pass to be added to the string builder + * @param additionalInfo additional info function which is executed for each widget + */ + public static > void print(T parent, Predicate test, NodeInfo additionalInfo) { + StringBuilder builder = new StringBuilder(); + if (parent instanceof IWidget) builder.append("Widget"); + else if (parent instanceof ResizeNode) builder.append("ResizeNode"); + else builder.append(parent.getClass()); + builder.append(" tree of ").append(parent).append('\n'); + ModularUI.LOGGER.info(toString(builder, parent, test, additionalInfo)); + } + + public static > String toString(T parent) { + return toString(parent, w -> true, null); + } + + public static > String toString(T parent, NodeInfo additionalInfo) { + return toString(parent, w -> true, additionalInfo); + } + + public static > String toString(T parent, Predicate test) { + return toString(parent, test, null); + } + + public static > String toString(T parent, Predicate test, NodeInfo additionalInfo) { + return toString(null, parent, test, additionalInfo).toString(); + } + + /** + * Writes the sub widget tree into a human-readable tree graph with Unicode characters. + * + * @param builder the string builder to add the tree to or null for a new builder + * @param parent starting point + * @param test test widgets have to pass to be added to the string builder + * @param additionalInfo additional info function which is executed for each widget + * @return the string builder which was used to build the graph + */ + public static > StringBuilder toString(StringBuilder builder, T parent, Predicate test, NodeInfo additionalInfo) { + if (builder == null) builder = new StringBuilder(); + getTree(parent, parent, test, builder, additionalInfo, "", false, null); + return builder; + } + + protected static > void getTree(T root, T parent, Predicate test, StringBuilder builder, NodeInfo additionalInfo, String indent, boolean hasNextSibling, Set visited) { + if (!indent.isEmpty()) { + builder.append(indent); + if (TreeUtil.allowUnicode) { + builder.append(hasNextSibling ? U_PIPE_MID : U_PIPE_END); + } else { + builder.append(hasNextSibling ? PIPE_MID : PIPE_END); + } + builder.append(' '); + } + if (visited == null) visited = new ReferenceOpenHashSet<>(); + if (visited.contains(parent)) { + builder.append("CYCLING TREE FOUND (").append(parent).append(")\n"); + return; + } + visited.add(parent); + builder.append(parent); + if (additionalInfo != null) { + builder.append(" {"); + additionalInfo.addInfo(root, parent, builder); + builder.append("}"); + } + builder.append('\n'); + if (parent.hasChildren()) { + List children = parent.getChildren(); + for (int i = 0; i < children.size(); i++) { + T child = children.get(i); + if (test.test(child)) { + String nextIndent = indent; + if (hasNextSibling) { + nextIndent += (TreeUtil.allowUnicode ? U_PIPE : PIPE) + ' '; + } else { + nextIndent += " "; + } + getTree(root, child, test, builder, additionalInfo, nextIndent, i < children.size() - 1, visited); + } + } + } + } + + public interface NodeInfo> { + + void addInfo(T root, T widget, StringBuilder builder); + + default NodeInfo combine(NodeInfo other, String joiner) { + return (root, widget, builder) -> { + addInfo(root, widget, builder); + builder.append(joiner); + other.addInfo(root, widget, builder); + }; + } + + default NodeInfo combine(NodeInfo other) { + return combine(other, " | "); + } + + @SafeVarargs + static > NodeInfo of(String joiner, NodeInfo... infos) { + return (root, widget, builder) -> { + for (int i = 0; i < infos.length; i++) { + NodeInfo info = infos[i]; + info.addInfo(root, widget, builder); + if (i < infos.length - 1) { + builder.append(joiner); + } + } + }; + } + + @SafeVarargs + static > NodeInfo of(NodeInfo... infos) { + return of(" | ", infos); + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java index e371a84c4..ce963e69e 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java @@ -21,6 +21,7 @@ * The widget provider as ran on both sides. Inside the provider sync handlers can be registered with variants of * {@link ISyncRegistrar#getOrCreateSyncHandler(String, int, Class, Supplier)}. */ +@ApiStatus.Obsolete public class DynamicSyncHandler extends SyncHandler implements IDynamicSyncNotifiable { private IWidgetProvider widgetProvider; diff --git a/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java b/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java index 1e2ecf821..36af16143 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java @@ -56,7 +56,7 @@ public boolean canHover() { IDrawable.isVisible(getHoverBackground()) || IDrawable.isVisible(getHoverOverlay()) || getTooltip() != null) return true; - WidgetThemeEntry widgetTheme = getWidgetTheme(getContext().getTheme()); + WidgetThemeEntry widgetTheme = getWidgetTheme(getPanel().getTheme()); if (getBackground() == null && IDrawable.isVisible(widgetTheme.getTheme().getBackground())) return true; return getHoverBackground() == null && IDrawable.isVisible(widgetTheme.getHoverTheme().getBackground()); } diff --git a/src/main/java/com/cleanroommc/modularui/widget/AbstractScrollWidget.java b/src/main/java/com/cleanroommc/modularui/widget/AbstractScrollWidget.java index 479388bc3..42bfe2377 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/AbstractScrollWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/AbstractScrollWidget.java @@ -67,7 +67,7 @@ public void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, @Override public void beforeResize(boolean onOpen) { super.beforeResize(onOpen); - this.scroll.applyWidgetTheme(getContext().getTheme().getScrollbarTheme().getTheme(isHovering())); + this.scroll.applyWidgetTheme(getPanel().getTheme().getScrollbarTheme().getTheme(isHovering())); if (onOpen) checkScrollbarActive(true); getScrollArea().getScrollPadding().scrollPaddingAll(0); applyAdditionalOffset(this.scroll.getScrollX()); @@ -137,7 +137,7 @@ public void preDraw(ModularGuiContext context, boolean transformed) { public void postDraw(ModularGuiContext context, boolean transformed) { if (!transformed) { Stencil.remove(); - WidgetThemeEntry scrollbarTheme = context.getTheme().getScrollbarTheme(); + WidgetThemeEntry scrollbarTheme = getPanel().getTheme().getScrollbarTheme(); this.scroll.drawScrollbar(context, scrollbarTheme.getTheme(isHovering()), scrollbarTheme.getTheme().getBackground()); } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/AbstractWidget.java b/src/main/java/com/cleanroommc/modularui/widget/AbstractWidget.java new file mode 100644 index 000000000..0165fb2d7 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/AbstractWidget.java @@ -0,0 +1,341 @@ +package com.cleanroommc.modularui.widget; + +import com.cleanroommc.modularui.api.widget.IDelegatingWidget; +import com.cleanroommc.modularui.api.widget.INotifyEnabled; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.ModularScreen; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; +import com.cleanroommc.modularui.widget.sizer.Area; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.MustBeInvokedByOverriders; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * Very basic implementation of {@link IWidget}. + */ +public abstract class AbstractWidget implements IWidget { + + // gui context + private boolean valid = false; + private IWidget parent = null; + private ModularPanel panel = null; + private ModularGuiContext context = null; + + @Nullable private String name; + private boolean enabled = true; + private int timeHovered = -1; + private int timeBelowMouse = -1; + + private final Area area = new Area(); + private StandardResizer resizer; + + /** + * Returns the screen of the panel of this widget is being opened in. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public ModularScreen getScreen() { + return getPanel().getScreen(); + } + + @Override + public void scheduleResize() { + this.resizer.markDirty(); + } + + @Override + public boolean requiresResize() { + return this.resizer.requiresResize(); + } + + /** + * Called when a panel is opened. Use {@link #onInit()} and {@link #afterInit()} for custom logic. + * + * @param parent the parent this element belongs to + * @param late true if this is called some time after the widget tree of the parent has been initialised + */ + @ApiStatus.Internal + @Override + public final void initialise(@NotNull IWidget parent, boolean late) { + this.timeHovered = -1; + this.timeBelowMouse = -1; + if (this.resizer == null) { + throw new IllegalStateException("Resizer must be set before the widget initializes! Affected widget: " + this); + } + if (!(this instanceof ModularPanel)) { + this.parent = parent; + this.panel = parent.getPanel(); + this.context = parent.getContext(); + getArea().z(parent.getArea().z() + 1); + if (parent instanceof AbstractWidget aw) { + this.resizer.initialize(aw.resizer, parent.getScreen().getResizeNode()); + } else { + this.resizer.initialize(parent.resizer(), parent.getScreen().getResizeNode()); + } + } + this.valid = true; + onInitInternal(late); + onInit(); + if (hasChildren()) { + for (IWidget child : getChildren()) { + child.initialise(this, false); + } + } + afterInit(); + onResized(); + } + + void onInitInternal(boolean late) {} + + /** + * Called after this widget is initialised and before the children are initialised. + */ + @ApiStatus.OverrideOnly + public void onInit() {} + + /** + * Called after this widget is initialised and after the children are initialised. + */ + @ApiStatus.OverrideOnly + public void afterInit() {} + + /** + * Called when this widget is removed from the widget tree or after the panel is closed. + * Overriding this is fine, but super must be called. + */ + @MustBeInvokedByOverriders + @Override + public void dispose() { + if (hasChildren()) { + for (IWidget child : getChildren()) { + child.dispose(); + } + } + if (!(this instanceof ModularPanel)) { + this.panel = null; + this.parent = null; + this.context = null; + } + resizer().dispose(); + this.timeHovered = -1; + this.timeBelowMouse = -1; + this.valid = false; + } + + // ------------------- + // === Gui context === + // ------------------- + + /** + * Returns if this widget is currently part of an open panel. Only if this is true information about parent, panel and gui context can + * be obtained. + * + * @return true if this widget is part of an open panel + */ + @Override + public boolean isValid() { + return valid; + } + + @Override + public void onUpdate() { + if (isHovering()) this.timeHovered++; + if (isBelowMouse()) this.timeBelowMouse++; + } + + @MustBeInvokedByOverriders + @Override + public void onMouseStartHover() { + this.timeHovered = 0; + } + + @MustBeInvokedByOverriders + @Override + public void onMouseEndHover() { + this.timeHovered = -1; + } + + @MustBeInvokedByOverriders + @Override + public void onMouseEnterArea() { + this.timeBelowMouse = 0; + } + + @MustBeInvokedByOverriders + @Override + public void onMouseLeaveArea() { + this.timeBelowMouse = -1; + } + + @Override + public boolean isHoveringFor(int ticks) { + return timeHovered >= ticks; + } + + @Override + public boolean isBelowMouseFor(int ticks) { + return timeBelowMouse >= ticks; + } + + public int getTicksHovered() { + return timeHovered; + } + + public int getTicksBelowMouse() { + return timeBelowMouse; + } + + /** + * Returns the area of this widget. This contains information such as position, size, relative position to parent, padding and margin. + * Even tho this is a mutable object, you should refrain from modifying the values. + * + * @return area of this widget + */ + @Override + public Area getArea() { + return area; + } + + /** + * Shortcut to get the area of the parent + * + * @return parent area + */ + public Area getParentArea() { + IWidget parent = getParent(); + while (parent instanceof IDelegatingWidget dw) { + parent = dw.getParent(); + } + return parent.getArea(); + } + + /** + * Returns if this widget is currently enabled. Disabled widgets (and all its children) are not rendered and can't be interacted with. + * + * @return true if this widget is enabled. + */ + @Override + public boolean isEnabled() { + return this.enabled; + } + + /** + * Sets enabled state. Disabled widgets (and all its children) are not rendered and can't be interacted with. + * + * @param enabled enabled state + */ + @Override + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + this.enabled = enabled; + if (isValid() && getParent() instanceof INotifyEnabled notifyEnabled) { + notifyEnabled.onChildChangeEnabled(this, enabled); + } + } + } + + /** + * Returns the parent of this widget. If this is a {@link ModularPanel} this will always return null contrary to the annotation. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public @NotNull IWidget getParent() { + if (!isValid()) { + throw new IllegalStateException(this + " is not in a valid state!"); + } + return parent; + } + + /** + * Returns the gui context of the screen this widget is part of. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public ModularGuiContext getContext() { + if (!isValid()) { + throw new IllegalStateException(this + " is not in a valid state!"); + } + return context; + } + + /** + * Used to set the gui context on panels internally. + */ + @ApiStatus.Internal + protected final void setContext(ModularGuiContext context) { + this.context = context; + } + + /** + * Returns the panel of this widget is being opened in. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public @NotNull ModularPanel getPanel() { + if (!isValid()) { + throw new IllegalStateException(this + " is not in a valid state!"); + } + return panel; + } + + @Override + public @NotNull StandardResizer resizer() { + return this.resizer; + } + + protected final StandardResizer rawResizer() { + return this.resizer; + } + + protected void resizer(StandardResizer resizer) { + Objects.requireNonNull(resizer); + if (this.resizer == resizer) return; + if (isValid() && this.resizer != null) { + resizer.replacementOf(this.resizer); + } + this.resizer = resizer; + } + + @Override + public @Nullable String getName() { + return name; + } + + protected void setName(String name) { + this.name = name; + } + + /** + * This is only used in {@link #toString()}. + * + * @return the simple class name or other fitting name + */ + protected String getTypeName() { + return getClass().getSimpleName(); + } + + /** + * @return the simple class plus the debug name if set + */ + @Override + public String toString() { + if (getName() != null) { + return getTypeName() + "#" + getName(); + } + return getTypeName(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/DelegatingSingleChildWidget.java b/src/main/java/com/cleanroommc/modularui/widget/DelegatingSingleChildWidget.java deleted file mode 100644 index 6c3e4cdb2..000000000 --- a/src/main/java/com/cleanroommc/modularui/widget/DelegatingSingleChildWidget.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.cleanroommc.modularui.widget; - -public class DelegatingSingleChildWidget> extends SingleChildWidget { - - @Override - public void onInit() { - super.onInit(); - if (hasChildren()) getChild().flex().relative(getParent()); - coverChildren(); - } - - @Override - public void postResize() { - super.postResize(); - if (hasChildren()) { - getArea().set(getChild().getArea()); - getArea().rx = getChild().getArea().rx; - getArea().ry = getChild().getArea().ry; - getChild().getArea().rx = 0; - getChild().getArea().ry = 0; - } - } -} diff --git a/src/main/java/com/cleanroommc/modularui/widget/DelegatingWidget.java b/src/main/java/com/cleanroommc/modularui/widget/DelegatingWidget.java new file mode 100644 index 000000000..622a11bb3 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/DelegatingWidget.java @@ -0,0 +1,107 @@ +package com.cleanroommc.modularui.widget; + +import com.cleanroommc.modularui.api.layout.IViewportStack; +import com.cleanroommc.modularui.api.widget.IDelegatingWidget; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.utils.MutableSingletonList; +import com.cleanroommc.modularui.widget.sizer.Area; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class DelegatingWidget extends AbstractWidget implements IDelegatingWidget { + + private final MutableSingletonList delegate = new MutableSingletonList<>(); + + public DelegatingWidget(IWidget delegate) { + this.delegate.set(delegate); + resizer(new StandardResizer(this)); + } + + protected void setDelegate(IWidget delegate) { + if (!this.delegate.isEmpty()) { + this.delegate.get().dispose(); + this.delegate.remove(); + } + if (delegate != null) { + this.delegate.set(delegate); + if (isValid()) { + initialise(getParent(), true); + delegate.scheduleResize(); + } + onChangeDelegate(delegate); + } + } + + protected void onChangeDelegate(IWidget delegate) {} + + @Override + public @NotNull List getChildren() { + return this.delegate; + } + + @Override + public void afterInit() { + super.resizer().setDefaultParent(null); // remove this widget from the resize node tree + if (hasChildren()) { + getDelegate().resizer().setDefaultParentIsDelegating(true); + getDelegate().resizer().relative(getParent()); // add the delegated widget at the place of this widget on the resize node tree + } + } + + @Override + public void postResize() { + super.postResize(); + if (getDelegate() != null) { + Area childArea = getDelegate().getArea(); + Area area = super.getArea(); + area.set(childArea); + area.rx = childArea.rx; + area.ry = childArea.ry; + childArea.rx = 0; + childArea.ry = 0; + } + } + + @Override + public @NotNull StandardResizer resizer() { + return getDelegate() != null ? getDelegate().resizer() : super.resizer(); + } + + @Override + public Area getArea() { + return getDelegate() != null ? getDelegate().getArea() : super.getArea(); + } + + @Override + public void transform(IViewportStack stack) { + stack.translate(super.getArea().rx, super.getArea().ry, 0); + } + + @Override + public boolean canBeSeen(IViewportStack stack) { + return false; + } + + @Override + public boolean requiresResize() { + return getDelegate() != null && getDelegate().requiresResize(); + } + + @Override + public int getDefaultWidth() { + return getDelegate() != null ? getDelegate().getDefaultWidth() : super.getDefaultWidth(); + } + + @Override + public int getDefaultHeight() { + return getDelegate() != null ? getDelegate().getDefaultHeight() : super.getDefaultHeight(); + } + + @Override + public IWidget getDelegate() { + return delegate.getOrNull(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/DragHandle.java b/src/main/java/com/cleanroommc/modularui/widget/DragHandle.java index d857bb504..b06f42e60 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/DragHandle.java +++ b/src/main/java/com/cleanroommc/modularui/widget/DragHandle.java @@ -3,7 +3,6 @@ import com.cleanroommc.modularui.api.layout.IViewport; import com.cleanroommc.modularui.api.layout.IViewportStack; import com.cleanroommc.modularui.api.widget.IDraggable; -import com.cleanroommc.modularui.api.widget.IGuiElement; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.DraggablePanelWrapper; import com.cleanroommc.modularui.screen.ModularPanel; @@ -58,7 +57,7 @@ public void onDrag(int mouseButton, long timeSinceLastClick) { } @Override - public boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + public boolean canDropHere(int x, int y, @Nullable IWidget widget) { return this.parentDraggable != null && this.parentDraggable.canDropHere(x, y, widget); } diff --git a/src/main/java/com/cleanroommc/modularui/widget/DraggableWidget.java b/src/main/java/com/cleanroommc/modularui/widget/DraggableWidget.java index 3772c7b81..39f4f55da 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/DraggableWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/DraggableWidget.java @@ -46,7 +46,7 @@ public boolean onDragStart(int mouseButton) { @Override public void onDragEnd(boolean successful) { if (successful) { - flex().top(getContext().getAbsMouseY() - this.relativeClickY) + resizer().top(getContext().getAbsMouseY() - this.relativeClickY) .left(getContext().getAbsMouseX() - this.relativeClickX); this.movingArea.x = getArea().x; this.movingArea.y = getArea().y; diff --git a/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java b/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java index b79dd3cd4..52ee8350a 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java @@ -1,6 +1,5 @@ package com.cleanroommc.modularui.widget; -import com.cleanroommc.modularui.api.layout.IResizeable; import com.cleanroommc.modularui.api.layout.IViewportStack; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.ModularPanel; @@ -8,7 +7,7 @@ import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.widget.sizer.Area; -import com.cleanroommc.modularui.widget.sizer.Flex; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -16,7 +15,7 @@ public class EmptyWidget implements IWidget { private final Area area = new Area(); - private final Flex flex = new Flex(this); + private final StandardResizer resizer = new StandardResizer(this); private boolean requiresResize = false; private boolean enabled = true; private IWidget parent; @@ -107,9 +106,6 @@ public boolean canHoverThrough() { return true; } - @Override - public void markTooltipDirty() {} - @Override public @NotNull IWidget getParent() { return this.parent; @@ -121,21 +117,8 @@ public ModularGuiContext getContext() { } @Override - public Flex flex() { - return this.flex; - } - - @Override - public @NotNull IResizeable resizer() { - return this.flex; - } - - @Override - public void resizer(IResizeable resizer) {} - - @Override - public Flex getFlex() { - return this.flex; + public @NotNull StandardResizer resizer() { + return this.resizer; } @Override diff --git a/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java index 80b0ccef7..512a642ff 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java +++ b/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java @@ -1,12 +1,11 @@ package com.cleanroommc.modularui.widget; import com.cleanroommc.modularui.api.GuiAxis; -import com.cleanroommc.modularui.api.layout.ILayoutWidget; -import com.cleanroommc.modularui.api.layout.IResizeable; import com.cleanroommc.modularui.api.layout.IViewport; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetThemeEntry; +import com.cleanroommc.modularui.widget.sizer.ResizeNode; import com.cleanroommc.modularui.widgets.layout.IExpander; import net.minecraft.client.renderer.GlStateManager; @@ -21,7 +20,6 @@ import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; -import java.util.function.Predicate; @ApiStatus.Internal class InternalWidgetTree { @@ -55,9 +53,10 @@ static T findChildAt(IWidget parent, Class type, String[] static void drawTree(IWidget parent, ModularGuiContext context, boolean ignoreEnabled, boolean drawBackground) { if (!parent.isEnabled() && !ignoreEnabled) return; if (parent.requiresResize()) { - WidgetTree.resizeInternal(parent, false); + WidgetTree.resizeInternal(parent.resizer(), false); } + float alpha = parent.getPanel().getAlpha(); IViewport viewport = parent instanceof IViewport ? (IViewport) parent : null; @@ -74,7 +73,7 @@ static void drawTree(IWidget parent, ModularGuiContext context, boolean ignoreEn if (canBeSeen) { // draw widget GlStateManager.color(1f, 1f, 1f, alpha); - WidgetThemeEntry widgetTheme = parent.getWidgetTheme(context.getTheme()); + WidgetThemeEntry widgetTheme = parent.getWidgetTheme(parent.getPanel().getTheme()); if (drawBackground) parent.drawBackground(context, widgetTheme); parent.draw(context, widgetTheme); parent.drawOverlay(context, widgetTheme); @@ -161,7 +160,7 @@ static void drawBackground(IWidget parent, ModularGuiContext context, boolean ig // draw widget GlStateManager.color(1f, 1f, 1f, alpha); - WidgetThemeEntry widgetTheme = parent.getWidgetTheme(context.getTheme()); + WidgetThemeEntry widgetTheme = parent.getWidgetTheme(parent.getPanel().getTheme()); parent.drawBackground(context, widgetTheme); GlStateManager.popMatrix(); @@ -189,30 +188,27 @@ static void drawTreeForeground(IWidget parent, ModularGuiContext context) { context.popMatrix(); } - static boolean resizeWidget(IWidget widget, boolean init, boolean onOpen, boolean isParentLayout) { + static boolean resize(ResizeNode resizer, boolean init, boolean onOpen, boolean isParentLayout) { boolean alreadyCalculated = false; // first try to resize this widget - IResizeable resizer = widget.resizer(); - ILayoutWidget layout = widget instanceof ILayoutWidget layoutWidget ? layoutWidget : null; - boolean isLayout = layout != null; + boolean isLayout = resizer.isLayout(); if (init) { - widget.beforeResize(onOpen); - resizer.initResizing(); + resizer.initResizing(onOpen); if (!isLayout) resizer.setLayoutDone(true); } else { // if this is not the first time check if this widget is already resized alreadyCalculated = resizer.isFullyCalculated(isParentLayout); } - boolean selfFullyCalculated = resizer.isSelfFullyCalculated() || resizer.resize(widget, isParentLayout); + boolean selfFullyCalculated = resizer.isSelfFullyCalculated() || resizer.resize(isParentLayout); - GuiAxis expandAxis = widget instanceof IExpander expander ? expander.getExpandAxis() : null; + GuiAxis expandAxis = resizer instanceof IExpander expander ? expander.getExpandAxis() : null; // now resize all children and collect children which could not be fully calculated - List anotherResize = Collections.emptyList(); - if (!resizer.areChildrenCalculated() && widget.hasChildren()) { + List anotherResize = Collections.emptyList(); + if (!resizer.areChildrenCalculated() && !resizer.getChildren().isEmpty()) { anotherResize = new ArrayList<>(); - for (IWidget child : widget.getChildren()) { - if (init) child.flex().checkExpanded(expandAxis); - if (!resizeWidget(child, init, onOpen, isLayout)) { + for (ResizeNode child : resizer.getChildren()) { + if (init) child.checkExpanded(expandAxis); + if (!resize(child, init, onOpen, isLayout)) { anotherResize.add(child); } } @@ -225,15 +221,15 @@ static boolean resizeWidget(IWidget widget, boolean init, boolean onOpen, boolea // we need to keep track of which widgets are not yet fully calculated, so we can call onResized on those which later are // fully calculated BitSet state = getCalculatedState(anotherResize, isLayout); - if (layout != null && shouldLayout) { - layoutSuccessful = layout.layoutWidgets(); + if (isLayout && shouldLayout) { + layoutSuccessful = resizer.layoutChildren(); } // post resize this widget if possible - resizer.postResize(widget); + resizer.postResize(); - if (layout != null && shouldLayout) { - layoutSuccessful &= layout.postLayoutWidgets(); + if (isLayout && shouldLayout) { + layoutSuccessful &= resizer.postLayoutChildren(); } if (shouldLayout) resizer.setLayoutDone(layoutSuccessful); checkFullyCalculated(anotherResize, state, isLayout); @@ -242,7 +238,7 @@ static boolean resizeWidget(IWidget widget, boolean init, boolean onOpen, boolea // now fully resize all children which needs it if (!anotherResize.isEmpty()) { for (int i = 0; i < anotherResize.size(); i++) { - if (resizeWidget(anotherResize.get(i), false, onOpen, isLayout)) { + if (resize(anotherResize.get(i), false, onOpen, isLayout)) { anotherResize.remove(i--); } } @@ -250,29 +246,29 @@ static boolean resizeWidget(IWidget widget, boolean init, boolean onOpen, boolea resizer.setChildrenResized(anotherResize.isEmpty()); selfFullyCalculated = resizer.isFullyCalculated(isParentLayout); - if (selfFullyCalculated && !alreadyCalculated) widget.onResized(); + if (selfFullyCalculated && !alreadyCalculated) resizer.onResized(); return selfFullyCalculated; } - private static BitSet getCalculatedState(List children, boolean isLayout) { + private static BitSet getCalculatedState(List children, boolean isLayout) { if (children.isEmpty()) return null; BitSet state = new BitSet(); for (int i = 0; i < children.size(); i++) { - IWidget widget = children.get(i); - if (widget.resizer().isFullyCalculated(isLayout)) { + ResizeNode widget = children.get(i); + if (widget.isFullyCalculated(isLayout)) { state.set(i); } } return state; } - private static void checkFullyCalculated(List children, BitSet state, boolean isLayout) { + private static void checkFullyCalculated(List children, BitSet state, boolean isLayout) { if (children.isEmpty() || state == null) return; int j = 0; for (int i = 0; i < children.size(); i++) { - IWidget widget = children.get(i); - if (!state.get(j) && widget.resizer().isFullyCalculated(isLayout)) { + ResizeNode widget = children.get(i); + if (!state.get(j) && widget.isFullyCalculated(isLayout)) { widget.onResized(); state.set(j); children.remove(i--); @@ -280,26 +276,4 @@ private static void checkFullyCalculated(List children, BitSet state, b j++; } } - - static void getTree(IWidget root, IWidget parent, Predicate test, StringBuilder builder, WidgetTree.WidgetInfo additionalInfo, String indent, boolean hasNextSibling) { - if (!indent.isEmpty()) { - builder.append(indent).append(hasNextSibling ? "├ " : "└ "); - } - builder.append(parent); - if (additionalInfo != null) { - builder.append(" {"); - additionalInfo.addInfo(root, parent, builder); - builder.append("}"); - } - builder.append('\n'); - if (parent.hasChildren()) { - List children = parent.getChildren(); - for (int i = 0; i < children.size(); i++) { - IWidget child = children.get(i); - if (test.test(child)) { - getTree(root, child, test, builder, additionalInfo, indent + (hasNextSibling ? "│ " : " "), i < children.size() - 1); - } - } - } - } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/RenderNode.java b/src/main/java/com/cleanroommc/modularui/widget/RenderNode.java new file mode 100644 index 000000000..01e331211 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/RenderNode.java @@ -0,0 +1,27 @@ +package com.cleanroommc.modularui.widget; + +import com.cleanroommc.modularui.api.widget.IWidget; + +import java.util.List; + +public class RenderNode implements WidgetNode { + + private IWidget linkedWidget; + private RenderNode parent; + private List children; + + @Override + public IWidget getWidget() { + return linkedWidget; + } + + @Override + public RenderNode getParent() { + return parent; + } + + @Override + public List getChildren() { + return children; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/Widget.java b/src/main/java/com/cleanroommc/modularui/widget/Widget.java index 0362c2613..8d23f9613 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/Widget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/Widget.java @@ -3,19 +3,15 @@ import com.cleanroommc.modularui.api.ITheme; import com.cleanroommc.modularui.api.IThemeApi; import com.cleanroommc.modularui.api.drawable.IDrawable; -import com.cleanroommc.modularui.api.layout.IResizeable; import com.cleanroommc.modularui.api.layout.IViewportStack; import com.cleanroommc.modularui.api.value.ISyncOrValue; import com.cleanroommc.modularui.api.value.IValue; import com.cleanroommc.modularui.api.widget.IDragResizeable; import com.cleanroommc.modularui.api.widget.IGuiAction; -import com.cleanroommc.modularui.api.widget.INotifyEnabled; import com.cleanroommc.modularui.api.widget.IPositioned; import com.cleanroommc.modularui.api.widget.ISynced; import com.cleanroommc.modularui.api.widget.ITooltip; import com.cleanroommc.modularui.api.widget.IWidget; -import com.cleanroommc.modularui.screen.ModularPanel; -import com.cleanroommc.modularui.screen.ModularScreen; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.theme.WidgetTheme; @@ -25,9 +21,8 @@ import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.value.sync.SyncHandler; import com.cleanroommc.modularui.value.sync.ValueSyncHandler; -import com.cleanroommc.modularui.widget.sizer.Area; -import com.cleanroommc.modularui.widget.sizer.Flex; -import com.cleanroommc.modularui.widget.sizer.IUnResizeable; +import com.cleanroommc.modularui.widget.sizer.Bounds; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -50,30 +45,18 @@ * * @param the type of this widget. This is used for proper return types in builder like methodsY */ -public class Widget> implements IWidget, IPositioned, ITooltip, ISynced { +public class Widget> extends AbstractWidget implements IPositioned, ITooltip, ISynced { // other - @Nullable private String name; - private boolean enabled = true; - private int timeHovered = -1; - private int timeBelowMouse = -1; private boolean excludeAreaInRecipeViewer = false; - // gui context - private boolean valid = false; - private IWidget parent = null; - private ModularPanel panel = null; - private ModularGuiContext context = null; // sizing - private final Area area = new Area(); - private final Flex flex = new Flex(this); - private IResizeable resizer = this.flex; private BiConsumer transform; - private boolean requiresResize = false; // syncing @Nullable private IValue value; @Nullable private String syncKey; @Nullable private SyncHandler syncHandler; // rendering + @Nullable private IDrawable shadow = null; @Nullable private IDrawable background = null; @Nullable private IDrawable overlay = null; @Nullable private IDrawable hoverBackground = null; @@ -84,64 +67,33 @@ public class Widget> implements IWidget, IPositioned, ITo @Nullable private List guiActionListeners; // TODO replace with proper event system @Nullable private Consumer onUpdateListener; + public Widget() { + resizer(new StandardResizer(this)); + } + // ----------------- // === Lifecycle === // ----------------- - /** - * Called when a panel is opened. Use {@link #onInit()} and {@link #afterInit()} for custom logic. - * - * @param parent the parent this element belongs to - * @param late true if this is called some time after the widget tree of the parent has been initialised - */ - @ApiStatus.Internal @Override - public final void initialise(@NotNull IWidget parent, boolean late) { - this.timeHovered = -1; - this.timeBelowMouse = -1; - if (!(this instanceof ModularPanel)) { - this.parent = parent; - this.panel = parent.getPanel(); - this.context = parent.getContext(); - getArea().z(parent.getArea().z() + 1); - if (this.guiActionListeners != null) { - for (IGuiAction action : this.guiActionListeners) { - this.context.getScreen().registerGuiActionListener(action); - } + void onInitInternal(boolean late) { + if (this.guiActionListeners != null) { + for (IGuiAction action : this.guiActionListeners) { + getContext().getScreen().registerGuiActionListener(action); } } + if (this.value != null && this.syncKey != null) { throw new IllegalStateException("Widget has a value and a sync key for a synced value. This is not allowed!"); } - this.valid = true; if (!getScreen().isClientOnly()) { initialiseSyncHandler(getScreen().getSyncManager(), late); } if (isExcludeAreaInRecipeViewer()) { getContext().getRecipeViewerSettings().addExclusionArea(this); } - onInit(); - if (hasChildren()) { - for (IWidget child : getChildren()) { - child.initialise(this, false); - } - } - afterInit(); - this.requiresResize = false; } - /** - * Called after this widget is initialised and before the children are initialised. - */ - @ApiStatus.OverrideOnly - public void onInit() {} - - /** - * Called after this widget is initialised and after the children are initialised. - */ - @ApiStatus.OverrideOnly - public void afterInit() {} - /** * Retrieves, verifies, and initialises a linked sync handler. * Custom logic should be handled in {@link #setSyncOrValue(ISyncOrValue)}. @@ -171,26 +123,14 @@ public void dispose() { if (isValid()) { if (this.guiActionListeners != null) { for (IGuiAction action : this.guiActionListeners) { - this.context.getScreen().removeGuiActionListener(action); + getScreen().removeGuiActionListener(action); } } if (isExcludeAreaInRecipeViewer()) { getContext().getRecipeViewerSettings().removeExclusionArea(this); } } - if (hasChildren()) { - for (IWidget child : getChildren()) { - child.dispose(); - } - } - if (!(this instanceof ModularPanel)) { - this.panel = null; - this.parent = null; - this.context = null; - } - this.timeHovered = -1; - this.timeBelowMouse = -1; - this.valid = false; + super.dispose(); } // ----------------- @@ -208,7 +148,10 @@ public void dispose() { */ @Override public void drawBackground(ModularGuiContext context, WidgetThemeEntry widgetTheme) { - IDrawable bg = getCurrentBackground(context.getTheme(), widgetTheme); + if (this.shadow != null) { + this.shadow.drawAtZero(context, getArea().width, getArea().height, getActiveWidgetTheme(widgetTheme, isHovering())); + } + IDrawable bg = getCurrentBackground(getPanel().getTheme(), widgetTheme); if (bg != null) { bg.drawAtZero(context, getArea().width, getArea().height, getActiveWidgetTheme(widgetTheme, isHovering())); } @@ -236,7 +179,7 @@ public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) {} */ @Override public void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetTheme) { - IDrawable bg = getCurrentOverlay(context.getTheme(), widgetTheme); + IDrawable bg = getCurrentOverlay(getPanel().getTheme(), widgetTheme); if (bg != null) { bg.drawAtZeroPadded(context, getArea(), getActiveWidgetTheme(widgetTheme, isHovering())); } @@ -360,7 +303,6 @@ public W tooltip(RichTooltip tooltip) { * Should be called when information which is displayed in the tooltip via {@link ITooltip#tooltipDynamic(Consumer)}. * It will invalidate the current tooltip and be caused to rebuild. */ - @Override public void markTooltipDirty() { if (this.tooltip != null) { this.tooltip.markDirty(); @@ -392,6 +334,10 @@ public final WidgetThemeEntry getWidgetTheme(ITheme theme) { return getWidgetThemeInternal(theme); } + public final @Nullable WidgetThemeKey getWidgetThemeOverride() { + return widgetThemeOverride; + } + /** * Returns the actual used widget theme. Uses {@link #widgetTheme(String)} if it has been set, otherwise calls * {@link #getWidgetThemeInternal(ITheme)} @@ -538,8 +484,7 @@ public W invisible() { @MustBeInvokedByOverriders @Override public void onUpdate() { - if (isHovering()) this.timeHovered++; - if (isBelowMouse()) this.timeBelowMouse++; + super.onUpdate(); if (this.onUpdateListener != null) { this.onUpdateListener.accept(getThis()); } @@ -570,7 +515,7 @@ public W listenGuiAction(IGuiAction action) { } this.guiActionListeners.add(action); if (isValid()) { - this.context.getScreen().registerGuiActionListener(action); + getScreen().registerGuiActionListener(action); } return getThis(); } @@ -622,96 +567,23 @@ public W setEnabledIf(Predicate condition) { // === Resizing === // ---------------- - @Override - public int getDefaultWidth() { - return isValid() ? getWidgetTheme(getContext().getTheme()).getTheme().getDefaultWidth() : 18; - } - - @Override - public int getDefaultHeight() { - return isValid() ? getWidgetTheme(getContext().getTheme()).getTheme().getDefaultHeight() : 18; - } - - @Override - public void scheduleResize() { - this.requiresResize = true; - } + public void estimateSize(Bounds bounds) { - @Override - public boolean requiresResize() { - return this.requiresResize; } - @MustBeInvokedByOverriders @Override - public void onResized() { - this.requiresResize = false; - } - - /** - * Returns the area of this widget. This contains information such as position, size, relative position to parent, padding and margin. - * Even tho this is a mutable object, you should refrain from modifying the values. - * - * @return area of this widget - */ - @Override - public Area getArea() { - return this.area; - } - - /** - * Returns the flex of this widget. This is responsible for calculating size, pos and relative pos. - * Originally this was intended to be modular for custom flex class. May come back to this in the future. - * Same as {@link #flex()}. - * - * @return flex of this widget - */ - @Override - public Flex getFlex() { - return this.flex; - } - - /** - * Returns the flex of this widget. This is responsible for calculating size, pos and relative pos. - * Originally this was intended to be modular for custom flex class. May come back to this in the future. - * Same as {@link #getFlex()}. - * - * @return flex of this widget - */ - @Override - public Flex flex() { - return getFlex(); - } - - /** - * Returns the resizer of this widget. This is actually the field responsible for resizing this widget. - * Within MUI this is always the same as {@link #flex()}. Custom resizer have not been tested. - * The relevance of separating flex and resizer is left to be investigated in the future. - * - * @return the resizer of this widget - */ - @NotNull - @Override - public IResizeable resizer() { - return this.resizer; + public int getDefaultWidth() { + return isValid() ? getWidgetTheme(getPanel().getTheme()).getTheme().getDefaultWidth() : 18; } - /** - * Sets the resizer of this widget, which is responsible for resizing this widget. - * Within MUI this setter is never used. Custom resizer have not been tested. - * The relevance of separating flex and resizer is left to be investigated in the future. - * - * @param resizer resizer - */ - @ApiStatus.Experimental @Override - public void resizer(IResizeable resizer) { - this.resizer = resizer != null ? resizer : IUnResizeable.INSTANCE; + public int getDefaultHeight() { + return isValid() ? getWidgetTheme(getPanel().getTheme()).getTheme().getDefaultHeight() : 18; } @Override public void transform(IViewportStack stack) { - IWidget.super.transform(stack); + super.transform(stack); if (this.transform != null) { this.transform.accept(getThis(), stack); } @@ -722,82 +594,6 @@ public W transform(BiConsumer transform) { return getThis(); } - // ------------------- - // === Gui context === - // ------------------- - - /** - * Returns if this widget is currently part of an open panel. Only if this is true information about parent, panel and gui context can - * be obtained. - * - * @return true if this widget is part of an open panel - */ - @Override - public final boolean isValid() { - return this.valid; - } - - /** - * Returns the screen of the panel of this widget is being opened in. - * - * @return the screen of this widget - * @throws IllegalStateException if {@link #isValid()} returns false - */ - @Override - public ModularScreen getScreen() { - return getPanel().getScreen(); - } - - /** - * Returns the panel of this widget is being opened in. - * - * @return the screen of this widget - * @throws IllegalStateException if {@link #isValid()} returns false - */ - @Override - public @NotNull ModularPanel getPanel() { - if (!isValid()) { - throw new IllegalStateException(this + " is not in a valid state!"); - } - return this.panel; - } - - /** - * Returns the parent of this widget. If this is a {@link ModularPanel} this will always return null contrary to the annotation. - * - * @return the screen of this widget - * @throws IllegalStateException if {@link #isValid()} returns false - */ - @Override - public @NotNull IWidget getParent() { - if (!isValid()) { - throw new IllegalStateException(this + " is not in a valid state!"); - } - return this.parent; - } - - /** - * Returns the gui context of the screen this widget is part of. - * - * @return the screen of this widget - * @throws IllegalStateException if {@link #isValid()} returns false - */ - @Override - public ModularGuiContext getContext() { - if (!isValid()) { - throw new IllegalStateException(this + " is not in a valid state!"); - } - return this.context; - } - - /** - * Used to set the gui context on panels internally. - */ - @ApiStatus.Internal - protected final void setContext(ModularGuiContext context) { - this.context = context; - } - // --------------- // === Syncing === // -------------- @@ -886,30 +682,6 @@ protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { // === Other === // ------------- - /** - * Returns if this widget is currently enabled. Disabled widgets (and all its children) are not rendered and can't be interacted with. - * - * @return true if this widget is enabled. - */ - @Override - public boolean isEnabled() { - return this.enabled; - } - - /** - * Sets enabled state. Disabled widgets (and all its children) are not rendered and can't be interacted with. - * - * @param enabled enabled state - */ - @Override - public void setEnabled(boolean enabled) { - if (this.enabled != enabled) { - this.enabled = enabled; - if (isValid() && getParent() instanceof INotifyEnabled notifyEnabled) { - notifyEnabled.onChildChangeEnabled(this, enabled); - } - } - } /** * Disables the widget from start. Useful inside widget tree creation, where widget references are usually not stored. @@ -921,48 +693,6 @@ public W disabled() { return getThis(); } - @MustBeInvokedByOverriders - @Override - public void onMouseStartHover() { - this.timeHovered = 0; - } - - @MustBeInvokedByOverriders - @Override - public void onMouseEndHover() { - this.timeHovered = -1; - } - - @MustBeInvokedByOverriders - @Override - public void onMouseEnterArea() { - this.timeBelowMouse = 0; - } - - @MustBeInvokedByOverriders - @Override - public void onMouseLeaveArea() { - this.timeBelowMouse = -1; - } - - @Override - public boolean isHoveringFor(int ticks) { - return timeHovered >= ticks; - } - - @Override - public boolean isBelowMouseFor(int ticks) { - return timeBelowMouse >= ticks; - } - - public int getTicksHovered() { - return timeHovered; - } - - public int getTicksBelowMouse() { - return timeBelowMouse; - } - @Override public Object getAdditionalHoverInfo(IViewportStack viewportStack, int mouseX, int mouseY) { if (this instanceof IDragResizeable dragResizeable) { @@ -1003,15 +733,10 @@ public W debugName(String name) { * @return this */ public W name(String name) { - this.name = name; + setName(name); return getThis(); } - @Override - public @Nullable String getName() { - return name; - } - /** * Returns this widget with proper generic type. * @@ -1022,24 +747,4 @@ public W name(String name) { public W getThis() { return (W) this; } - - /** - * This is only used in {@link #toString()}. - * - * @return the simple class name or other fitting name - */ - protected String getTypeName() { - return getClass().getSimpleName(); - } - - /** - * @return the simple class plus the debug name if set - */ - @Override - public String toString() { - if (getName() != null) { - return getTypeName() + "#" + getName(); - } - return getTypeName(); - } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/WidgetNode.java b/src/main/java/com/cleanroommc/modularui/widget/WidgetNode.java new file mode 100644 index 000000000..39fa99bea --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/WidgetNode.java @@ -0,0 +1,14 @@ +package com.cleanroommc.modularui.widget; + +import com.cleanroommc.modularui.api.widget.IWidget; + +import java.util.List; + +public interface WidgetNode { + + IWidget getWidget(); + + T getParent(); + + List getChildren(); +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java index 2d703a2df..87acc6e9b 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java +++ b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java @@ -3,39 +3,30 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.MCHelper; import com.cleanroommc.modularui.api.drawable.IKey; -import com.cleanroommc.modularui.api.layout.ILayoutWidget; import com.cleanroommc.modularui.api.widget.ISynced; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.ModularPanel; import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; import com.cleanroommc.modularui.utils.NumberFormat; -import com.cleanroommc.modularui.utils.ObjectList; +import com.cleanroommc.modularui.utils.TreeUtil; import com.cleanroommc.modularui.value.sync.ModularSyncManager; import com.cleanroommc.modularui.value.sync.PanelSyncManager; +import com.cleanroommc.modularui.widget.sizer.ResizeNode; import net.minecraft.util.text.TextComponentString; -import com.google.common.collect.AbstractIterator; -import com.google.common.collect.Streams; import org.apache.commons.lang3.mutable.MutableInt; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnmodifiableView; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; import java.util.NoSuchElementException; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; +import java.util.Set; /** * Helper class to perform operations on widget trees such as traversing, drawing, resizing, finding widgets and printing it. */ -public class WidgetTree { +public class WidgetTree extends TreeUtil { /** * If this variable is true, the time it takes to resize a sub widget tree is logged each time. @@ -43,7 +34,7 @@ public class WidgetTree { */ public static boolean logResizeTime = false; - public static final WidgetInfo INFO_AREA = (root, widget, builder) -> builder + public static final WidgetInfo WIDGET_INFO_AREA = (root, widget, builder) -> builder .append("Area xywh:") .append(widget.getArea().x - root.getArea().x) .append(", ") @@ -51,284 +42,15 @@ public class WidgetTree { .append(", ") .append(widget.getArea().width) .append(", ") - .append(widget.getArea().height); - public static final WidgetInfo INFO_ENABLED = (root, widget, builder) -> builder.append("Enabled: ").append(widget.isEnabled()); - public static final WidgetInfo INFO_FULLY_RESIZED = (root, widget, builder) -> builder - .append("Fully resized: ") - .append(widget.resizer().isFullyCalculated(widget.hasParent() && widget.getParent() instanceof ILayoutWidget)); - public static final WidgetInfo INFO_RESIZED_DETAILED = (root, widget, builder) -> builder - .append("Self resized: ").append(widget.resizer().isSelfFullyCalculated(widget.hasParent() && widget.getParent() instanceof ILayoutWidget)) - .append(", Is pos final: ").append(!widget.resizer().canRelayout(widget.hasParent() && widget.getParent() instanceof ILayoutWidget)) - .append(", Children resized: ").append(widget.resizer().areChildrenCalculated()) - .append(", Layout done: ").append(widget.resizer().isLayoutDone()); - public static final WidgetInfo INFO_RESIZED_COLLAPSED = (root, widget, builder) -> { - if (widget.resizer().isFullyCalculated(widget.hasParent() && widget.getParent() instanceof ILayoutWidget)) { - INFO_FULLY_RESIZED.addInfo(root, widget, builder); - } else { - INFO_RESIZED_DETAILED.addInfo(root, widget, builder); - } - }; - public static final WidgetInfo INFO_WIDGET_THEME = (root, widget, builder) -> builder.append("Widget theme: ") - .append(widget.getWidgetTheme(widget.getContext().getTheme()).getKey().getFullName()); + .append(widget.getArea().height) + .append(", rx: ").append(widget.getArea().rx) + .append(", ry: ").append(widget.getArea().ry); + public static final WidgetInfo WIDGET_INFO_ENABLED = (root, widget, builder) -> builder.append("Enabled: ").append(widget.isEnabled()); + public static final WidgetInfo WIDGET_INFO_WIDGET_THEME = (root, widget, builder) -> builder.append("Widget theme: ") + .append(widget.getWidgetTheme(widget.getPanel().getTheme()).getKey().getFullName()); private WidgetTree() {} - public static List getAllChildrenByLayer(IWidget parent) { - return getAllChildrenByLayer(parent, false); - } - - public static List getAllChildrenByLayer(IWidget parent, boolean includeSelf) { - List children = new ArrayList<>(); - if (includeSelf) children.add(parent); - ObjectList parents = ObjectList.create(); - parents.add(parent); - while (!parents.isEmpty()) { - for (IWidget child : parents.removeFirst().getChildren()) { - if (!child.getChildren().isEmpty()) { - parents.add(child); - } - children.add(child); - } - } - return children; - } - - public static boolean foreachChildBFS(IWidget parent, Predicate consumer) { - return foreachChildBFS(parent, consumer, false); - } - - /** - * Iterates through the whole sub widget tree by using breath-first-search. - *

- * This method delivers good performance and can outperform {@link #foreachChild(IWidget, Predicate, boolean)} in certain small widget - * trees. - * - * @param parent starting point - * @param consumer Operation on each child. Return false to terminate the iteration. - * @param includeSelf true if the consumer should also consume the parent - * @return true if the iteration was not terminated by the consumer - */ - public static boolean foreachChildBFS(IWidget parent, Predicate consumer, boolean includeSelf) { - if (includeSelf && !consumer.test(parent)) return false; - ObjectList parents = ObjectList.create(); - parents.add(parent); - while (!parents.isEmpty()) { - for (IWidget child : parents.removeFirst().getChildren()) { - if (child.hasChildren()) { - parents.addLast(child); - } - if (!consumer.test(child)) return false; - } - } - return true; - } - - /** - * @see #foreachChild(IWidget, Predicate, boolean) - */ - public static boolean foreachChild(IWidget parent, Predicate consumer) { - return foreachChild(parent, consumer, false); - } - - /** - * Iterates through the whole sub widget tree recursively. - *

- * This method has the best performance in most cases, but can be outperformed on certain small widget trees. - * - * @param parent starting point - * @param consumer Operation on each child. Return false to terminate the iteration. - * @param includeSelf true if the consumer should also consume the parent - * @return true if the iteration was not terminated by the consumer - */ - public static boolean foreachChild(IWidget parent, Predicate consumer, boolean includeSelf) { - if (includeSelf && !consumer.test(parent)) return false; - if (!parent.hasChildren()) return true; - for (IWidget widget : parent.getChildren()) { - if (!consumer.test(widget)) return false; - if (widget.hasChildren() && !foreachChild(widget, consumer, false)) { - return false; - } - } - return true; - } - - /** - * Iterates through the whole sub widget tree recursively. Unlike {@link #foreachChild(IWidget, Predicate, boolean)}, which can only - * return a boolean, this method can return any type. Once the consumer returns a non-null value, the iteration is terminated and the - * value will be returned. - * - * @param parent starting point - * @param consumer Operation on each child. Return a non-null value to terminate the iteration and to return the value. - * @param includeSelf true if the consumer should also consume the parent - * @return the first resulting value of the consumer or null of it always returned null - */ - public static @Nullable T foreachChildWithResult(IWidget parent, Function consumer, boolean includeSelf) { - if (includeSelf) { - T t = consumer.apply(parent); - if (t != null) return t; - } - if (!parent.hasChildren()) return null; - for (IWidget widget : parent.getChildren()) { - T t = consumer.apply(widget); - if (t != null) return t; - if (widget.hasChildren()) { - t = foreachChildWithResult(widget, consumer, false); - if (t != null) return t; - } - } - return null; - } - - public static boolean foreachChildReverse(IWidget parent, Predicate consumer, boolean includeSelf) { - if (parent.getChildren().isEmpty()) { - return !includeSelf || consumer.test(parent); - } - for (IWidget widget : parent.getChildren()) { - if (!widget.getChildren().isEmpty() && foreachChildReverse(widget, consumer, false)) { - return false; - } - if (!consumer.test(widget)) return false; - } - return !includeSelf || consumer.test(parent); - } - - /** - * Creates a flat stream of the whole sub widget tree. - *

- * {@link Stream#forEach(Consumer)} on this has slightly worse performance than {@link #foreachChildBFS(IWidget, Predicate, boolean)} on - * small widget trees and has similar performance on large widget trees. The performance is significantly better than - * {@link #iteratorBFS(IWidget)} even though this method uses it. - * - * @param parent starting point. - * @return stream of the sub widget tree - */ - @SuppressWarnings("UnstableApiUsage") - public static Stream flatStream(IWidget parent) { - if (!parent.hasChildren()) return Stream.of(parent); - return Streams.stream(iteratorBFS(parent)); - } - - public static @UnmodifiableView Iterable iterableBFS(IWidget parent) { - return () -> iteratorBFS(parent); - } - - /** - * Creates an unmodifiable iterator of the whole sub widget tree. - *

- * This method of iterating has the worst performance in every case. It's roughly 4 times worse than - * {@link #foreachChildBFS(IWidget, Predicate, boolean)}. If not used extensively the performance is still nothing to worry about. - * - * @param parent starting point - * @return an unmodifiable iterator of the sub widget tree - */ - public static @UnmodifiableView Iterator iteratorBFS(IWidget parent) { - return new AbstractIterator<>() { - - private final ObjectList queue = ObjectList.create(); - private Iterator currentIt; - - @Override - protected IWidget computeNext() { - if (currentIt == null) { - currentIt = parent.getChildren().iterator(); - return parent; - } - if (currentIt.hasNext()) return handleWidget(currentIt.next()); - while (!queue.isEmpty()) { - currentIt = queue.removeFirst().getChildren().iterator(); - if (currentIt.hasNext()) return handleWidget(currentIt.next()); - } - return endOfData(); - } - - private IWidget handleWidget(IWidget widget) { - if (widget.hasChildren()) { - queue.addLast(widget); - } - return widget; - } - }; - } - - /** - * Finds all widgets in the sub widget tree which match the test. - * - * @param parent starting point - * @param test test which the target widgets have to pass - * @return a list of matching widgets - */ - public static List collectWidgets(IWidget parent, Predicate test) { - List widgets = new ArrayList<>(); - foreachChild(parent, w -> { - if (test.test(w)) widgets.add(w); - return true; - }, true); - return widgets; - } - - public static List collectWidgetsByType(IWidget parent, Class type) { - return collectWidgetsByType(parent, type, null); - } - - /** - * Finds all widgets in the sub widget tree which match the given type and additional test. - * - * @param parent starting point - * @param type type of the target widgets - * @param test test which the target widgets have to pass - * @param type of the target widgets - * @return a list of matching widgets - */ - @SuppressWarnings("unchecked") - public static List collectWidgetsByType(IWidget parent, Class type, @Nullable Predicate test) { - List widgets = new ArrayList<>(); - foreachChild(parent, w -> { - if (w.isType(type)) { - T t = (T) w; - if (test == null || test.test(t)) widgets.add(t); - } - return true; - }, true); - return widgets; - } - - /** - * Finds the first widget in the sub widget tree, for which the test returns true. - * - * @param parent starting point - * @param test test which the widget has to pass - * @return the first matching widget - */ - public static IWidget findFirst(IWidget parent, @NotNull Predicate test) { - return foreachChildWithResult(parent, w -> { - if (test.test(w)) { - return w; - } - return null; - }, true); - } - - /** - * Finds the first widget in the sub widget tree with the given type, for which the test returns true. - * - * @param parent starting point - * @param type type of the target widget - * @param test test which the widget has to pass - * @return the first matching widget - */ - @SuppressWarnings("unchecked") - public static T findFirst(IWidget parent, Class type, @Nullable Predicate test) { - return foreachChildWithResult(parent, w -> { - if (w.isType(type)) { - T t = (T) w; - if (test == null || test.test(t)) { - return t; - } - } - return null; - }, true); - } - /** * Finds the first widget in the sub widget tree that matches the given name. * @@ -453,36 +175,6 @@ public static T findFirst(IWidget parent, Class type, @Nu return InternalWidgetTree.findChildAt(parent, type, path, 0, false); } - public static void applyPos(IWidget parent) { - WidgetTree.foreachChildBFS(parent, child -> { - child.resizer().applyPos(child); - return true; - }, true); - } - - public static IWidget findParent(IWidget parent, Predicate filter) { - if (parent == null) return null; - while (!(parent instanceof ModularPanel)) { - if (filter.test(parent)) { - return parent; - } - parent = parent.getParent(); - } - return filter.test(parent) ? parent : null; - } - - @SuppressWarnings("unchecked") - public static T findParent(IWidget parent, Class type) { - if (parent == null) return null; - while (!(parent instanceof ModularPanel)) { - if (type.isAssignableFrom(parent.getClass())) { - return (T) parent; - } - parent = parent.getParent(); - } - return type.isAssignableFrom(parent.getClass()) ? (T) parent : null; - } - public static boolean hasSyncedValues(ModularPanel panel) { return !foreachChild(panel, widget -> !(widget instanceof ISynced synced) || !synced.isSynced(), true); } @@ -548,149 +240,74 @@ public static void resize(IWidget parent) { } @ApiStatus.Internal - public static void resizeInternal(IWidget parent, boolean onOpen) { - long fullTime = System.nanoTime(); + public static void resizeInternal(ResizeNode parent, boolean onOpen) { + long time = System.nanoTime(); // check if updating this widget's pos and size can potentially update its parents - while (!(parent instanceof ModularPanel) && (parent.getParent() instanceof ILayoutWidget || parent.getParent().flex().dependsOnChildren())) { + while (parent.getParent() != null && (parent.getParent().dependsOnChildren() || parent.getParent().isLayout())) { parent = parent.getParent(); } - long rawTime = System.nanoTime(); // resize each widget and calculate their relative pos - if (!InternalWidgetTree.resizeWidget(parent, true, onOpen, false) && !InternalWidgetTree.resizeWidget(parent, false, onOpen, false)) { - if (MCHelper.getPlayer() != null) { - MCHelper.getPlayer().sendMessage(new TextComponentString(IKey.RED + "ModularUI: Failed to resize sub tree of widget '" - + parent + "' of screen '" + parent.getScreen().toString() + "'. See log for more info.")); + try { + if (!InternalWidgetTree.resize(parent, true, onOpen, false) && !InternalWidgetTree.resize(parent, false, onOpen, false)) { + if (MCHelper.getPlayer() != null) { + MCHelper.getPlayer().sendMessage(new TextComponentString(IKey.RED + "ModularUI: Failed to resize sub tree of " + + parent.getDebugDisplayName() + ". See log for more info.")); + } + ModularUI.LOGGER.error("Failed to resize widget. Affected resize node tree:"); + print(parent, RESIZE_NODE_INFO_RESIZED_COLLAPSED); } - ModularUI.LOGGER.error("Failed to resize widget. Affected widget tree:"); - printTree(parent, INFO_RESIZED_COLLAPSED); + // now apply the calculated pos + preApplyPos(parent); + applyPos(parent); + postFullResize(parent); + } catch (Throwable e) { + ModularUI.LOGGER.fatal("An exception was thrown while resizing widgets. Exception:"); + ModularUI.LOGGER.catching(e); + ModularUI.LOGGER.fatal("Affected node tree:"); + print(parent, RESIZE_NODE_INFO_RESIZED_COLLAPSED); } - rawTime = System.nanoTime() - rawTime; - // now apply the calculated pos - applyPos(parent); - WidgetTree.foreachChildBFS(parent, child -> { - child.postResize(); - return true; - }, true); if (WidgetTree.logResizeTime) { - fullTime = System.nanoTime() - fullTime; - ModularUI.LOGGER.info("Resized widget tree in {}s and {}s for full resize.", - NumberFormat.formatNanos(rawTime), - NumberFormat.formatNanos(fullTime)); + time = System.nanoTime() - time; + ModularUI.LOGGER.info("Resized widget tree in {}s.", NumberFormat.formatNanos(time)); } } - /** - * Prints the whole sub widget tree to the log as a human-readable tree graph with unicode characters. You may need to enabled unicode - * characters in your IDE terminal to display them properly. - * - * @param parent starting point - */ - public static void printTree(IWidget parent) { - printTree(parent, w -> true, null); - } - - /** - * Prints the whole sub widget tree to the log as a human-readable tree graph with unicode characters. You may need to enabled unicode - * characters in your IDE terminal to display them properly. - * - * @param parent starting point - * @param additionalInfo additional info function which is executed for each widget - */ - public static void printTree(IWidget parent, WidgetInfo additionalInfo) { - printTree(parent, w -> true, additionalInfo); - } - - /** - * Prints the whole sub widget tree to the log as a human-readable tree graph with unicode characters. You may need to enabled unicode - * characters in your IDE terminal to display them properly. - * - * @param parent starting point - * @param test test widgets have to pass to be added to the string builder - */ - public static void printTree(IWidget parent, Predicate test) { - printTree(parent, test, null); - } - - /** - * Prints the whole sub widget tree to the log as a human-readable tree graph with unicode characters. You may need to enabled unicode - * characters in your IDE terminal to display them properly. - * - * @param parent starting point - * @param test test widgets have to pass to be added to the string builder - * @param additionalInfo additional info function which is executed for each widget - */ - public static void printTree(IWidget parent, Predicate test, WidgetInfo additionalInfo) { - StringBuilder builder = new StringBuilder("Widget tree of ") - .append(parent) - .append('\n'); - ModularUI.LOGGER.info(widgetTreeToString(builder, parent, test, additionalInfo)); - } - - public static String widgetTreeToString(IWidget parent) { - return widgetTreeToString(parent, w -> true, null); - } - - public static String widgetTreeToString(IWidget parent, WidgetInfo additionalInfo) { - return widgetTreeToString(parent, w -> true, additionalInfo); + public static void preApplyPos(ResizeNode parent) { + parent.preApplyPos(); + for (ResizeNode resizeNode : parent.getChildren()) { + preApplyPos(resizeNode); + } } - public static String widgetTreeToString(IWidget parent, Predicate test) { - return widgetTreeToString(parent, test, null); + public static void applyPos(ResizeNode parent) { + parent.applyPos(); + for (ResizeNode resizeNode : parent.getChildren()) { + applyPos(resizeNode); + } } - public static String widgetTreeToString(IWidget parent, Predicate test, WidgetInfo additionalInfo) { - return widgetTreeToString(null, parent, test, additionalInfo).toString(); + public static void postFullResize(ResizeNode parent) { + parent.postFullResize(); + for (ResizeNode resizeNode : parent.getChildren()) { + postFullResize(resizeNode); + } } - /** - * Writes the sub widget tree into a human-readable tree graph with unicode characters. - * - * @param builder the string builder to add the tree to or null for a new builder - * @param parent starting point - * @param test test widgets have to pass to be added to the string builder - * @param additionalInfo additional info function which is executed for each widget - * @return the string builder which was used to build the graph - */ - public static StringBuilder widgetTreeToString(StringBuilder builder, IWidget parent, Predicate test, WidgetInfo additionalInfo) { - if (builder == null) builder = new StringBuilder(); - InternalWidgetTree.getTree(parent, parent, test, builder, additionalInfo, "", false); - return builder; + public static void verifyTree(ResizeNode parent, Set visited) { + if (visited.contains(parent)) { + throw new IllegalStateException("Found cycling resize node dependencies!"); + } + visited.add(parent); + if (!parent.getChildren().isEmpty()) { + for (ResizeNode child : parent.getChildren()) { + verifyTree(child, visited); + } + } } /** * An interface to add information of a widget to a string builder. */ - public interface WidgetInfo { - - void addInfo(IWidget root, IWidget widget, StringBuilder builder); - - default WidgetInfo combine(WidgetInfo other, String joiner) { - return (root, widget, builder) -> { - addInfo(root, widget, builder); - builder.append(joiner); - other.addInfo(root, widget, builder); - }; - } - - default WidgetInfo combine(WidgetInfo other) { - return combine(other, " | "); - } - - static WidgetInfo of(String joiner, WidgetInfo... infos) { - return (root, widget, builder) -> { - for (int i = 0; i < infos.length; i++) { - WidgetInfo info = infos[i]; - info.addInfo(root, widget, builder); - if (i < infos.length - 1) { - builder.append(joiner); - } - } - }; - } - - static WidgetInfo of(WidgetInfo... infos) { - return of(" | ", infos); - } - } + public interface WidgetInfo extends NodeInfo {} } diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java index ba9a83cf1..8687af01d 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/Area.java @@ -3,7 +3,6 @@ import com.cleanroommc.modularui.animation.IAnimatable; import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.api.layout.IViewportStack; -import com.cleanroommc.modularui.api.widget.IGuiElement; import com.cleanroommc.modularui.utils.Interpolations; import com.cleanroommc.modularui.utils.MathUtils; @@ -17,7 +16,7 @@ * A rectangular widget area, composed of a position and a size. * Also has fields for a relative position, a layer and margin & padding. */ -public class Area extends Rectangle implements IUnResizeable, IAnimatable { +public class Area extends Rectangle implements IAnimatable { public static boolean isInside(int x, int y, int w, int h, int px, int py) { SHARED.set(x, y, w, h); @@ -31,7 +30,8 @@ public static boolean isInside(int x, int y, int w, int h, int px, int py) { /** * relative position (in most cases the direct parent) */ - public int rx, ry; + public int rx; + public int ry; /** * the widget layer within this panel */ @@ -473,7 +473,7 @@ public void set(Rectangle area) { /** * Transforms the four corners of this rectangle with the given pose stack. The new rectangle can be rotated. - * Then a min fit rectangle, which is not rotated and aligned with the screen, is put around the corners. + * Then a min fit rectangle, which is aligned with the screen axis, is put around the corners. * * @param stack pose stack */ @@ -495,17 +495,6 @@ public Box getPadding() { return this.padding; } - @Override - public boolean resize(IGuiElement guiElement, boolean isParentLayout) { - guiElement.getArea().set(this); - return true; - } - - @Override - public Area getArea() { - return this; - } - /** * This creates a copy with size, pos, margin padding and z layer. * @@ -520,8 +509,10 @@ public String toString() { return "Area{" + "x=" + this.x + ", y=" + this.y + - ", width=" + this.width + - ", height=" + this.height + + ", w=" + this.width + + ", h=" + this.height + + ", rx=" + this.rx + + ", ry=" + this.ry + '}'; } diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/AreaResizer.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/AreaResizer.java new file mode 100644 index 000000000..70c82bc18 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/AreaResizer.java @@ -0,0 +1,25 @@ +package com.cleanroommc.modularui.widget.sizer; + +public class AreaResizer extends StaticResizer { + + private final Area area; + + public AreaResizer(Area area) { + this.area = area; + } + + @Override + public Area getArea() { + return area; + } + + @Override + public String getDebugDisplayName() { + return ""; + } + + @Override + public String toString() { + return "AreaResizer(" + this.area + ")"; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/Bounds.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/Bounds.java new file mode 100644 index 000000000..f28b6b4f9 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/Bounds.java @@ -0,0 +1,50 @@ +package com.cleanroommc.modularui.widget.sizer; + +public class Bounds { + + public static final int UNLIMITED_MAX = Integer.MAX_VALUE; + public static final int UNLIMITED_MIN = Integer.MIN_VALUE; + + private int minWidth = UNLIMITED_MIN, minHeight = UNLIMITED_MIN; + private int maxWidth = UNLIMITED_MAX, maxHeight = UNLIMITED_MAX; + + public Bounds set(int minWidth, int minHeight, int maxWidth, int maxHeight) { + this.minWidth = minWidth; + this.minHeight = minHeight; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + return this; + } + + public Bounds max(int width, int height) { + this.maxWidth = width; + this.maxHeight = height; + return this; + } + + public Bounds min(int width, int height) { + this.minWidth = width; + this.minHeight = height; + return this; + } + + public Bounds exact(int w, int h) { + return set(w, h, w, h); + } + + public int getMaxHeight() { + return maxHeight; + } + + public int getMaxWidth() { + return maxWidth; + } + + public int getMinHeight() { + return minHeight; + } + + public int getMinWidth() { + return minWidth; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/DelegatingResizer.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/DelegatingResizer.java new file mode 100644 index 000000000..ca765635d --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/DelegatingResizer.java @@ -0,0 +1,12 @@ +package com.cleanroommc.modularui.widget.sizer; + +import com.cleanroommc.modularui.api.widget.IWidget; + +public class DelegatingResizer extends StandardResizer { + + public DelegatingResizer(IWidget widget) { + super(widget); + } + + +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/DimensionSizer.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/DimensionSizer.java index c8d92221c..b5f8af4d9 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/sizer/DimensionSizer.java +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/DimensionSizer.java @@ -4,8 +4,7 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.ModularUIConfig; import com.cleanroommc.modularui.api.GuiAxis; -import com.cleanroommc.modularui.api.layout.IResizeable; -import com.cleanroommc.modularui.api.widget.IGuiElement; +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.network.NetworkUtils; import org.jetbrains.annotations.ApiStatus; @@ -19,6 +18,7 @@ @ApiStatus.Internal public class DimensionSizer { + private final ResizeNode resizer; private final GuiAxis axis; private final Unit p1 = new Unit(), p2 = new Unit(); @@ -32,7 +32,8 @@ public class DimensionSizer { private boolean marginPaddingApplied = false; private boolean canRelayout = false; - public DimensionSizer(GuiAxis axis) { + public DimensionSizer(ResizeNode resizer, GuiAxis axis) { + this.resizer = resizer; this.axis = axis; } @@ -73,7 +74,7 @@ public void resetSize() { } } - public void setCoverChildren(boolean coverChildren, IGuiElement widget) { + public void setCoverChildren(boolean coverChildren, IWidget widget) { getSize(widget); this.coverChildren = coverChildren; } @@ -141,9 +142,13 @@ public boolean dependsOnChildren() { } public boolean dependsOnParent() { - return !this.coverChildren && (this.end != null || + if (this.coverChildren) { + // if we cover children we ignore size config + return this.end != null || (this.start != null && this.start.isRelative()); + } + return this.end != null || (this.start != null && this.start.isRelative()) || - (this.size != null && this.size.isRelative())); + (this.size != null && this.size.isRelative()); } public void setResized(boolean all) { @@ -157,7 +162,7 @@ public void setResized(boolean pos, boolean size) { } public boolean isMarginPaddingApplied() { - return marginPaddingApplied; + return this.marginPaddingApplied; } public void setMarginPaddingApplied(boolean marginPaddingApplied) { @@ -168,15 +173,20 @@ private boolean needsSize(Unit unit) { return unit.isRelative() && unit.getAnchor() != 0; } - public void apply(Area area, IResizeable relativeTo, IntSupplier defaultSize) { - // is already calculated - if (this.sizeCalculated && this.posCalculated) return; + public boolean test() { + return resizer != null && axis.isVertical() && resizer.toString().contains("menu_list"); + } + + public void apply(Area area, ResizeNode relativeTo, IntSupplier defaultSize) { + boolean sizeCalculated = isSizeCalculated(); + boolean posCalculated = isPosCalculated(); + if (sizeCalculated && posCalculated) return; int p, s; int parentSize = relativeTo.getArea().getSize(this.axis); boolean calcParent = relativeTo.isSizeCalculated(this.axis); Box padding = relativeTo.getArea().getPadding(); - if (this.sizeCalculated && !this.posCalculated) { + if (sizeCalculated) { // pos not calculated // size was calculated before s = area.getSize(this.axis); if (this.start != null) { @@ -184,9 +194,10 @@ public void apply(Area area, IResizeable relativeTo, IntSupplier defaultSize) { } else if (this.end != null) { p = calcPoint(this.end, padding, s, parentSize, calcParent) - s; } else { - throw new IllegalStateException(); + p = 0; + this.posCalculated = true; } - } else if (!this.sizeCalculated && this.posCalculated) { + } else if (posCalculated) { // size not calculated // pos was calculated before p = area.getRelativePoint(this.axis); if (this.size != null) { @@ -195,7 +206,7 @@ public void apply(Area area, IResizeable relativeTo, IntSupplier defaultSize) { s = defaultSize.getAsInt(); this.sizeCalculated = s > 0; } - } else { + } else { // pos and size not calculated // calc start, end and size if (this.start == null && this.end == null) { p = 0; @@ -252,7 +263,7 @@ public void apply(Area area, IResizeable relativeTo, IntSupplier defaultSize) { s = Math.min(s, parentSize /*- padding.getTotal(this.axis)*/ - margin.getTotal(this.axis)); } area.setRelativePoint(this.axis, p); - area.setPoint(this.axis, p + relativeTo.getArea().x); // temporary + area.setPoint(this.axis, p + relativeTo.getArea().getPoint(this.axis)); // temporary area.setSize(this.axis, s); } @@ -298,40 +309,40 @@ public void coverChildrenForEmpty(Area area, Area relativeTo) { } } - public void applyMarginAndPaddingToPos(IGuiElement parent, Area area, Area relativeTo) { + public void applyMarginAndPaddingToPos(IWidget parent, Area area, Area relativeTo) { // apply self margin and parent padding if not done yet if (isMarginPaddingApplied()) return; setMarginPaddingApplied(true); - int left = area.getMargin().getStart(this.axis) + relativeTo.getPadding().getStart(this.axis); - int right = area.getMargin().getEnd(this.axis) + relativeTo.getPadding().getEnd(this.axis); - if (left > 0 && ((this.start != null && !this.start.isRelative()) || + int start = area.getMargin().getStart(this.axis) + relativeTo.getPadding().getStart(this.axis); + int end = area.getMargin().getEnd(this.axis) + relativeTo.getPadding().getEnd(this.axis); + if (start > 0 && ((this.start != null && !this.start.isRelative()) || (this.end != null && !this.end.isRelative() && (this.size == null || !this.size.isRelative())))) { - left = 0; + start = 0; } - if (right > 0 && ((this.end != null && !this.end.isRelative()) || + if (end > 0 && ((this.end != null && !this.end.isRelative()) || (this.start != null && !this.start.isRelative() && (this.size == null || !this.size.isRelative())))) { - right = 0; + end = 0; } - if (left == 0 && right == 0) return; + if (start == 0 && end == 0) return; int parentS = relativeTo.getSize(this.axis); int s = area.getSize(this.axis); int rp = area.getRelativePoint(this.axis); // relative pos - if (left > 0) { - if (right > 0) { - if (left + right + s > parentS) { + if (start > 0) { + if (end > 0) { + if (start + end + s > parentS) { // widget and margin + padding is larger than available space - area.setRelativePoint(this.axis, left); + area.setRelativePoint(this.axis, start); GuiError.throwNew(parent, GuiError.Type.SIZING, "Margin/padding is set on both sides on axis " + this.axis + ", but total size exceeds parent size."); return; } - if (right > parentS - s - rp) area.setRelativePoint(this.axis, parentS - right - s); - else if (left > rp) area.setRelativePoint(this.axis, left); + if (end > parentS - s - rp) area.setRelativePoint(this.axis, parentS - end - s); + else if (start > rp) area.setRelativePoint(this.axis, start); return; } - if (left > rp) area.setRelativePoint(this.axis, left); - } else if (right > 0) { - if (right > parentS - s - rp) area.setRelativePoint(this.axis, parentS - right - s); + if (start > rp) area.setRelativePoint(this.axis, start); + } else if (end > 0) { + if (end > parentS - s - rp) area.setRelativePoint(this.axis, parentS - end - s); } } @@ -372,7 +383,7 @@ public int calcPoint(Unit p, Box padding, int width, int parentSize, boolean par * @param newState the new unit type for the found unit * @return a used or unused unit. */ - private Unit getNext(IGuiElement widget, Unit.State newState) { + private Unit getNext(IWidget widget, Unit.State newState) { Unit ret = this.next; Unit other = ret == this.p1 ? this.p2 : this.p1; if (ret.state != Unit.State.UNUSED) { @@ -392,21 +403,21 @@ private Unit getNext(IGuiElement widget, Unit.State newState) { return ret; } - protected Unit getStart(IGuiElement widget) { + protected Unit getStart(IWidget widget) { if (this.start == null) { this.start = getNext(widget, Unit.State.START); } return this.start; } - protected Unit getEnd(IGuiElement widget) { + protected Unit getEnd(IWidget widget) { if (this.end == null) { this.end = getNext(widget, Unit.State.END); } return this.end; } - protected Unit getSize(IGuiElement widget) { + protected Unit getSize(IWidget widget) { if (this.size == null) { this.size = getNext(widget, Unit.State.SIZE); } diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/ExpanderResizer.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/ExpanderResizer.java new file mode 100644 index 000000000..29de66572 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/ExpanderResizer.java @@ -0,0 +1,20 @@ +package com.cleanroommc.modularui.widget.sizer; + +import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.widgets.layout.IExpander; + +public class ExpanderResizer extends StandardResizer implements IExpander { + + private final GuiAxis axis; + + public ExpanderResizer(IWidget widget, GuiAxis axis) { + super(widget); + this.axis = axis; + } + + @Override + public GuiAxis getExpandAxis() { + return axis; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/IUnResizeable.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/IUnResizeable.java deleted file mode 100644 index d31517294..000000000 --- a/src/main/java/com/cleanroommc/modularui/widget/sizer/IUnResizeable.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.cleanroommc.modularui.widget.sizer; - -import com.cleanroommc.modularui.api.layout.IResizeable; -import com.cleanroommc.modularui.api.widget.IGuiElement; - -/** - * A variation of {@link IResizeable} with default implementations which don't do anything - */ -public interface IUnResizeable extends IResizeable { - - IUnResizeable INSTANCE = new IUnResizeable() { - @Override - public boolean resize(IGuiElement guiElement, boolean isParentLayout) { - return true; - } - - @Override - public Area getArea() { - Area.SHARED.set(0, 0, 0, 0); - return Area.SHARED; - } - }; - - @Override - default void initResizing() {} - - @Override - default boolean postResize(IGuiElement guiElement) { - return true; - } - - @Override - default boolean isXCalculated() { - return true; - } - - @Override - default boolean isYCalculated() { - return true; - } - - @Override - default boolean isWidthCalculated() { - return true; - } - - @Override - default boolean isHeightCalculated() { - return true; - } - - @Override - default boolean areChildrenCalculated() { - return true; - } - - @Override - default boolean isLayoutDone() { - return true; - } - - @Override - default boolean canRelayout(boolean isParentLayout) { - return false; - } - - @Override - default void setChildrenResized(boolean resized) {} - - @Override - default void setLayoutDone(boolean done) {} - - @Override - default void setResized(boolean x, boolean y, boolean w, boolean h) {} - - @Override - default void setXMarginPaddingApplied(boolean b) {} - - @Override - default void setYMarginPaddingApplied(boolean b) {} - - @Override - default boolean isXMarginPaddingApplied() { - return true; - } - - @Override - default boolean isYMarginPaddingApplied() { - return true; - } -} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/ResizeNode.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/ResizeNode.java new file mode 100644 index 000000000..12692547c --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/ResizeNode.java @@ -0,0 +1,249 @@ +package com.cleanroommc.modularui.widget.sizer; + +import com.cleanroommc.modularui.api.GuiAxis; +import com.cleanroommc.modularui.api.ITreeNode; +import com.cleanroommc.modularui.api.layout.IResizeable; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ResizeNode implements IResizeable, ITreeNode { + + private ResizeNode defaultParent; + private ResizeNode parentOverride; + private final List children = new ArrayList<>(); + private boolean defaultParentIsDelegating = false; + private boolean requiresResize = true; + + @ApiStatus.Internal + @Override + public List getChildren() { + return children; + } + + @Override + public ResizeNode getParent() { + return parentOverride != null ? parentOverride : defaultParent; + } + + @ApiStatus.Internal + public void replacementOf(ResizeNode node) { + if (this == node) return; + //ModularUI.LOGGER.info("Replacing resizer node {} with node {}", node, this); + // remove this node from the tree by removing itself from the parents and removing the children + if (this.defaultParent != null) this.defaultParent.children.remove(this); + if (this.parentOverride != null) this.parentOverride.children.remove(this); + for (ResizeNode n : this.children) { + if (n.parentOverride == this) { + n.setParentOverride(null); + } + if (n.defaultParent == this) { + n.setDefaultParent(null); + } + } + this.children.clear(); + + // remove the node to replace from its tree and remember the exact position + int defI = -1; + int ovrI = -1; + if (node.defaultParent != null) { + defI = node.defaultParent.children.indexOf(node); + if (defI >= 0) node.defaultParent.children.remove(defI); + } + if (node.parentOverride != null) { + ovrI = node.parentOverride.children.indexOf(node); + if (ovrI >= 0) node.parentOverride.children.remove(ovrI); + } + // take over the parent and ourselves to the new parents with the remembered position + this.defaultParent = node.defaultParent; + this.parentOverride = node.parentOverride; + if (this.parentOverride != null) { + if (ovrI < 0) throw new IllegalStateException(); + this.parentOverride.children.add(ovrI, this); + } + if (this.defaultParent != null) { + if (defI < 0) throw new IllegalStateException(); + this.defaultParent.children.add(ovrI, this); + } + // take all children and update their parent to ourselves + this.children.addAll(node.children); + for (ResizeNode n : this.children) { + if (n.parentOverride == node) n.parentOverride = this; + if (n.defaultParent == node) n.defaultParent = this; + } + // finally invalidate replaced node + node.dispose(); + } + + public void dispose() { + if (getParent() != null) getParent().children.remove(this); + this.defaultParent = null; + this.parentOverride = null; + this.children.clear(); + } + + private boolean removeFromParent(ResizeNode parent, ResizeNode parent2, ResizeNode replacement) { + if (parent != null) { + if (parent == replacement) return true; + parent.children.remove(this); + } else if (parent2 != null) { + if (parent2 == replacement) return true; + parent2.children.remove(this); + } + return false; + } + + @ApiStatus.Internal + public void initialize(ResizeNode defaultParent, ResizeNode root) { + setDefaultParent(defaultParent); + } + + @ApiStatus.Internal + public void setDefaultParent(ResizeNode resizeNode) { + //ModularUI.LOGGER.info("Set default parent of {} to {}. Current: default: {}, override: {}", this, resizeNode, this.defaultParent, this.parentOverride); + if (resizeNode == this) throw new IllegalArgumentException("Tried to set itself as default parent in " + this); + if (removeFromParent(this.defaultParent, null, resizeNode)) return; + this.defaultParent = resizeNode; + if (this.parentOverride == null && resizeNode != null) { + resizeNode.children.add(this); + } + } + + protected void setParentOverride(ResizeNode resizeNode) { + //ModularUI.LOGGER.info("Set override parent of {} to {}. Current: default: {}, override: {}", this, resizeNode, this.defaultParent, this.parentOverride); + if (resizeNode == this) throw new IllegalArgumentException("Tried to set itself as parent override in " + this); + if (removeFromParent(this.parentOverride, this.defaultParent, resizeNode)) return; + this.parentOverride = resizeNode; + if (this.parentOverride != null) { + this.parentOverride.children.add(this); + } else if (this.defaultParent != null) { + this.defaultParent.children.add(this); + } + } + + @ApiStatus.Internal + public void setDefaultParentIsDelegating(boolean defaultParentIsDelegating) { + this.defaultParentIsDelegating = defaultParentIsDelegating; + } + + public boolean hasParentOverride() { + return this.parentOverride != null; + } + + @Override + public void initResizing(boolean onOpen) { + if (this.defaultParentIsDelegating && this.parentOverride != null) { + this.defaultParent.initResizing(onOpen); + } + } + + public void reset() {} + + public void markDirty() { + this.requiresResize = true; + } + + public void onResized() { + this.requiresResize = false; + if (this.defaultParentIsDelegating && this.parentOverride != null) { + this.defaultParent.onResized(); + } + } + + public void postFullResize() { + if (this.defaultParentIsDelegating && this.parentOverride != null) { + this.defaultParent.postFullResize(); + } + } + + public boolean requiresResize() { + return this.requiresResize; + } + + public boolean dependsOnParentX() { + return false; + } + + public boolean dependsOnParentY() { + return false; + } + + public boolean dependsOnParent() { + return dependsOnParentX() || dependsOnParentY(); + } + + public boolean dependsOnParent(GuiAxis axis) { + return axis.isHorizontal() ? dependsOnParentX() : dependsOnParentY(); + } + + public boolean dependsOnChildrenX() { + return false; + } + + public boolean dependsOnChildrenY() { + return false; + } + + public boolean dependsOnChildren() { + return dependsOnChildrenX() || dependsOnChildrenY(); + } + + public boolean dependsOnChildren(GuiAxis axis) { + return axis.isHorizontal() ? dependsOnChildrenX() : dependsOnChildrenY(); + } + + public boolean isSameResizer(ResizeNode node) { + return node == this; + } + + public boolean isLayout() { + return false; + } + + public boolean layoutChildren() { + return true; + } + + public boolean postLayoutChildren() { + return true; + } + + @ApiStatus.Internal + public void checkExpanded(@Nullable GuiAxis axis) {} + + public abstract boolean hasYPos(); + + public abstract boolean hasXPos(); + + public abstract boolean hasHeight(); + + public abstract boolean hasWidth(); + + public abstract boolean hasStartPos(GuiAxis axis); + + public abstract boolean hasEndPos(GuiAxis axis); + + public boolean hasPos(GuiAxis axis) { + return axis.isHorizontal() ? hasXPos() : hasYPos(); + } + + public boolean hasSize(GuiAxis axis) { + return axis.isHorizontal() ? hasWidth() : hasHeight(); + } + + public boolean isExpanded() { + return false; + } + + public abstract boolean isFullSize(); + + public abstract boolean hasFixedSize(); + + public abstract String getDebugDisplayName(); + + @Override + public abstract String toString(); +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/ScreenResizeNode.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/ScreenResizeNode.java new file mode 100644 index 000000000..cd9f6ebf4 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/ScreenResizeNode.java @@ -0,0 +1,31 @@ +package com.cleanroommc.modularui.widget.sizer; + +import com.cleanroommc.modularui.screen.ModularScreen; + +public class ScreenResizeNode extends StaticResizer { + + private final ModularScreen screen; + + public ScreenResizeNode(ModularScreen screen) { + this.screen = screen; + } + + public ModularScreen getScreen() { + return screen; + } + + @Override + public Area getArea() { + return screen.getScreenArea(); + } + + @Override + public String getDebugDisplayName() { + return "screen '" + this.screen + "'"; + } + + @Override + public String toString() { + return "ScreenResizeNode(" + this.screen + ")"; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/Flex.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/StandardResizer.java similarity index 68% rename from src/main/java/com/cleanroommc/modularui/widget/sizer/Flex.java rename to src/main/java/com/cleanroommc/modularui/widget/sizer/StandardResizer.java index 9b8806269..948093891 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/sizer/Flex.java +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/StandardResizer.java @@ -4,7 +4,7 @@ import com.cleanroommc.modularui.api.GuiAxis; import com.cleanroommc.modularui.api.layout.ILayoutWidget; import com.cleanroommc.modularui.api.layout.IResizeable; -import com.cleanroommc.modularui.api.widget.IGuiElement; +import com.cleanroommc.modularui.api.widget.IDelegatingWidget; import com.cleanroommc.modularui.api.widget.IPositioned; import com.cleanroommc.modularui.api.widget.IVanillaSlot; import com.cleanroommc.modularui.api.widget.IWidget; @@ -18,60 +18,52 @@ import java.util.List; import java.util.function.DoubleSupplier; -/** - * This class handles resizing and positioning of widgets. - */ -public class Flex implements IResizeable, IPositioned { +public class StandardResizer extends WidgetResizeNode implements IPositioned { private final DimensionSizer x; private final DimensionSizer y; private boolean expanded = false; - private final IGuiElement parent; - private Area relativeTo; - private boolean relativeToParent = true; - private boolean bypassLayerRestriction = false; - private boolean childrenCalculated = false; - private boolean layoutDone = true; + private boolean childrenResized = false; + private boolean layoutResized = false; + private boolean relativeToScreen = false; - public Flex(IGuiElement parent) { - this.parent = parent; + public StandardResizer(IWidget widget) { + super(widget); this.x = createDimensionSizer(GuiAxis.X); this.y = createDimensionSizer(GuiAxis.Y); } protected DimensionSizer createDimensionSizer(GuiAxis axis) { - return new DimensionSizer(axis); - } - - public void reset() { - this.x.reset(); - this.y.reset(); - } - - public void resetPosition() { - this.x.resetPosition(); - this.y.resetPosition(); + return new DimensionSizer(this, axis); } @Override - public Flex flex() { - return this; + public void initialize(ResizeNode defaultParent, ResizeNode root) { + super.initialize(defaultParent, root); + if (this.relativeToScreen) { + setParentOverride(root); + } } @Override - public Area getArea() { - return this.parent.getArea(); + public void initResizing(boolean onOpen) { + setMarginPaddingApplied(false); + setResized(false); + this.childrenResized = false; + this.layoutResized = false; + super.initResizing(onOpen); } @Override - public boolean requiresResize() { - return this.parent.requiresResize(); + public void reset() { + this.x.reset(); + this.y.reset(); } - @Override - public void scheduleResize() { - this.parent.scheduleResize(); + public void resetPosition() { + this.x.resetPosition(); + this.y.resetPosition(); } @Override @@ -96,12 +88,12 @@ public boolean isHeightCalculated() { @Override public boolean areChildrenCalculated() { - return this.childrenCalculated; + return this.childrenResized; } @Override public boolean isLayoutDone() { - return this.layoutDone; + return this.layoutResized; } @Override @@ -110,282 +102,27 @@ public boolean canRelayout(boolean isParentLayout) { } @Override - public void setChildrenResized(boolean resized) { - this.childrenCalculated = resized; + public boolean isXMarginPaddingApplied() { + return this.x.isMarginPaddingApplied(); } @Override - public void setLayoutDone(boolean done) { - this.layoutDone = done; - } - - public Flex coverChildrenWidth() { - this.x.setCoverChildren(true, this.parent); - scheduleResize(); - return this; - } - - public Flex coverChildrenHeight() { - this.y.setCoverChildren(true, this.parent); - scheduleResize(); - return this; - } - - public Flex cancelMovementX() { - this.x.setCancelAutoMovement(true); - scheduleResize(); - return this; - } - - public Flex cancelMovementY() { - this.y.setCancelAutoMovement(true); - scheduleResize(); - return this; - } - - public Flex expanded() { - this.expanded = true; - scheduleResize(); - return this; - } - - public Flex relative(Area guiElement) { - this.relativeTo = guiElement; - this.relativeToParent = false; - scheduleResize(); - return this; - } - - public Flex relativeToScreen() { - this.relativeTo = null; - this.relativeToParent = false; - scheduleResize(); - return this; - } - - public Flex relativeToParent() { - this.relativeToParent = true; - scheduleResize(); - return this; + public boolean isYMarginPaddingApplied() { + return this.y.isMarginPaddingApplied(); } @Override - public Flex bypassLayerRestriction() { - this.bypassLayerRestriction = true; - scheduleResize(); - return this; - } - - @ApiStatus.Internal - public Flex left(float x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getLeft(), x, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex left(DoubleSupplier x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getLeft(), x, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex right(float x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getRight(), x, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex right(DoubleSupplier x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getRight(), x, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex top(float y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getTop(), y, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex top(DoubleSupplier y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getTop(), y, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex bottom(float y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getBottom(), y, offset, anchor, measure, autoAnchor); - } - - @ApiStatus.Internal - public Flex bottom(DoubleSupplier y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - return unit(getBottom(), y, offset, anchor, measure, autoAnchor); - } - - private Flex unit(Unit u, float val, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - u.setValue(val); - u.setMeasure(measure); - u.setOffset(offset); - u.setAnchor(anchor); - u.setAutoAnchor(autoAnchor); - scheduleResize(); - return this; - } - - private Flex unit(Unit u, DoubleSupplier val, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { - u.setValue(val); - u.setMeasure(measure); - u.setOffset(offset); - u.setAnchor(anchor); - u.setAutoAnchor(autoAnchor); - scheduleResize(); - return this; - } - - @ApiStatus.Internal - public Flex width(float val, int offset, Unit.Measure measure) { - return unitSize(getWidth(), val, offset, measure); - } - - @ApiStatus.Internal - public Flex width(DoubleSupplier val, int offset, Unit.Measure measure) { - return unitSize(getWidth(), val, offset, measure); - } - - @ApiStatus.Internal - public Flex height(float val, int offset, Unit.Measure measure) { - return unitSize(getHeight(), val, offset, measure); - } - - @ApiStatus.Internal - public Flex height(DoubleSupplier val, int offset, Unit.Measure measure) { - return unitSize(getHeight(), val, offset, measure); - } - - private Flex unitSize(Unit u, float val, int offset, Unit.Measure measure) { - u.setValue(val); - u.setMeasure(measure); - u.setOffset(offset); - scheduleResize(); - return this; - } - - private Flex unitSize(Unit u, DoubleSupplier val, int offset, Unit.Measure measure) { - u.setValue(val); - u.setMeasure(measure); - u.setOffset(offset); - scheduleResize(); - return this; - } - - public Flex anchorLeft(float val) { - getLeft().setAnchor(val); - getLeft().setAutoAnchor(false); - scheduleResize(); - return this; - } - - public Flex anchorRight(float val) { - getRight().setAnchor(1 - val); - getRight().setAutoAnchor(false); - scheduleResize(); - return this; - } - - public Flex anchorTop(float val) { - getTop().setAnchor(val); - getTop().setAutoAnchor(false); - scheduleResize(); + public StandardResizer resizer() { return this; } - public Flex anchorBottom(float val) { - getBottom().setAnchor(1 - val); - getBottom().setAutoAnchor(false); - scheduleResize(); - return this; - } - - public Flex anchor(Alignment alignment) { - if (this.x.hasStart() || !this.x.hasEnd()) { - anchorLeft(alignment.x); - } else if (this.x.hasEnd()) { - anchorRight(alignment.x); - } - if (this.y.hasStart() || !this.y.hasEnd()) { - anchorTop(alignment.y); - } else if (this.y.hasEnd()) { - anchorBottom(alignment.y); - } - return this; - } - - public void setUnit(Unit unit, GuiAxis axis, Unit.State pos) { - (axis.isHorizontal() ? this.x : this.y).setUnit(unit, pos); - } - - private IResizeable getRelativeTo() { - IGuiElement parent = this.parent.getParent(); - IResizeable relativeTo = this.relativeToParent && parent != null ? parent.resizer() : this.relativeTo; - return relativeTo != null ? relativeTo : this.parent.getScreen().getScreenArea(); - } - - public boolean isExpanded() { - return this.expanded; - } - - public boolean hasYPos() { - return this.y.hasPos(); - } - - public boolean hasXPos() { - return this.x.hasPos(); - } - - public boolean hasHeight() { - return this.y.hasSize(); - } - - public boolean hasWidth() { - return this.x.hasSize(); - } - - public boolean hasStartPos(GuiAxis axis) { - return axis.isHorizontal() ? this.x.hasStart() : this.y.hasStart(); - } - - public boolean hasEndPos(GuiAxis axis) { - return axis.isHorizontal() ? this.x.hasEnd() : this.y.hasEnd(); - } - - public boolean hasPos(GuiAxis axis) { - return axis.isHorizontal() ? hasXPos() : hasYPos(); - } - - public boolean hasSize(GuiAxis axis) { - return axis.isHorizontal() ? hasWidth() : hasHeight(); - } - - public boolean xAxisDependsOnChildren() { - return this.x.dependsOnChildren(); - } - - public boolean yAxisDependsOnChildren() { - return this.y.dependsOnChildren(); - } - - public boolean dependsOnChildren(GuiAxis axis) { - return axis.isHorizontal() ? xAxisDependsOnChildren() : yAxisDependsOnChildren(); - } - - public boolean dependsOnChildren() { - return xAxisDependsOnChildren() || yAxisDependsOnChildren(); - } - - public boolean hasFixedSize() { - return this.x.hasFixedSize() && this.y.hasFixedSize(); - } - - public boolean isFullSize() { - if (!hasHeight() || !hasWidth()) return false; - return this.x.isFullSize() && this.y.isFullSize(); + @Override + public void scheduleResize() { + markDirty(); } @ApiStatus.Internal + @Override public void checkExpanded(@Nullable GuiAxis axis) { this.x.setExpanded(false); this.y.setExpanded(false); @@ -396,70 +133,26 @@ public void checkExpanded(@Nullable GuiAxis axis) { } @Override - public void initResizing() { - setMarginPaddingApplied(false); - setResized(false); - this.childrenCalculated = false; - this.layoutDone = false; - } - - @Override - public void setResized(boolean x, boolean y, boolean w, boolean h) { - this.x.setResized(x, w); - this.y.setResized(y, h); - } - - @Override - public void setXMarginPaddingApplied(boolean b) { - this.x.setMarginPaddingApplied(b); - } - - @Override - public void setYMarginPaddingApplied(boolean b) { - this.y.setMarginPaddingApplied(b); - } - - @Override - public boolean isXMarginPaddingApplied() { - return this.x.isMarginPaddingApplied(); - } - - @Override - public boolean isYMarginPaddingApplied() { - return this.y.isMarginPaddingApplied(); - } - - @Override - public boolean resize(IGuiElement guiElement, boolean isParentLayout) { - IResizeable relativeTo = getRelativeTo(); - Area relativeArea = relativeTo.getArea(); - - /*if (!this.bypassLayerRestriction && (relativeArea.z() >= this.parent.getArea().z())) { - Area area = guiElement.getArea(); - area.setSize(18, 18); - area.rx = 0; - area.ry = 0; - guiElement.resizer().setResized(true); - GuiError.throwNew(this.parent, GuiError.Type.SIZING, "Widget can't be relative to a widget at the same level or above"); - return true; - }*/ - + public boolean resize(boolean isParentLayout) { + Area area = getArea(); + ResizeNode relativeTo = getParent(); // calculate x, y, width and height if possible - this.x.apply(guiElement.getArea(), relativeTo, guiElement::getDefaultWidth); - this.y.apply(guiElement.getArea(), relativeTo, guiElement::getDefaultHeight); + this.x.apply(area, relativeTo, () -> getWidget().getDefaultWidth()); + this.y.apply(area, relativeTo, () -> getWidget().getDefaultHeight()); return isFullyCalculated(isParentLayout); } @Override - public boolean postResize(IGuiElement guiElement) { + public boolean postResize() { boolean coverWidth = this.x.dependsOnChildren(); boolean coverHeight = this.y.dependsOnChildren(); if (!coverWidth && !coverHeight) return isSelfFullyCalculated(); - if (!(this.parent instanceof IWidget widget) || !widget.hasChildren()) { + IWidget widget = getWidget(); + if (!widget.hasChildren()) { coverChildrenForEmpty(); return isSelfFullyCalculated(); } - if (this.parent instanceof ILayoutWidget layout) { + if (getWidget() instanceof ILayoutWidget layout) { // layout widgets handle widget layout's themselves, so we only need to fit the right and bottom border coverChildrenForLayout(layout, widget); return isSelfFullyCalculated(); @@ -469,10 +162,10 @@ public boolean postResize(IGuiElement guiElement) { // this means for each edge there is at least one widget that touches it (plus padding and margin) // children are now calculated and now this area can be calculated if it requires childrens area - List children = widget.getChildren(); + List children = widget.getChildren(); // TODO cover resizer children instead? int moveChildrenX = 0, moveChildrenY = 0; - Box padding = this.parent.getArea().getPadding(); + Box padding = getWidget().getArea().getPadding(); // first calculate the area the children span int x0 = Integer.MAX_VALUE, x1 = Integer.MIN_VALUE, y0 = Integer.MAX_VALUE, y1 = Integer.MIN_VALUE; int w = 0, h = 0; @@ -480,10 +173,10 @@ public boolean postResize(IGuiElement guiElement) { boolean hasIndependentChildY = false; for (IWidget child : children) { Box margin = child.getArea().getMargin(); - IResizeable resizeable = child.resizer(); + ResizeNode resizeable = child.resizer(); Area area = child.getArea(); if (coverWidth) { - if (!child.flex().x.dependsOnParent()) { + if (!resizeable.dependsOnParentX()) { hasIndependentChildX = true; if (resizeable.isWidthCalculated() && resizeable.isXCalculated()) { w = Math.max(w, area.requestedWidth() + padding.horizontal()); @@ -495,7 +188,7 @@ public boolean postResize(IGuiElement guiElement) { } } if (coverHeight) { - if (!child.flex().y.dependsOnParent()) { + if (!resizeable.dependsOnParentY()) { hasIndependentChildY = true; if (resizeable.isHeightCalculated() && resizeable.isYCalculated()) { h = Math.max(h, area.requestedHeight() + padding.vertical()); @@ -508,7 +201,7 @@ public boolean postResize(IGuiElement guiElement) { } } if ((coverWidth && !hasIndependentChildX) || (coverHeight && !hasIndependentChildY)) { - GuiError.throwNew(this.parent, GuiError.Type.SIZING, "Can't cover children when all children depend on their parent!"); + GuiError.throwNew(getWidget(), GuiError.Type.SIZING, "Can't cover children when all children depend on their parent!"); return false; } if (x1 == Integer.MIN_VALUE) x1 = 0; @@ -519,20 +212,20 @@ public boolean postResize(IGuiElement guiElement) { if (h > y1 - y0) y1 = y0 + h; // now calculate new x, y, width and height based on the children area - Area relativeTo = getRelativeTo().getArea(); + Area relativeTo = getParent().getArea(); if (coverWidth) { // apply the size to this widget // the return value is the amount of pixels we need to move the children - moveChildrenX = this.x.postApply(this.parent.getArea(), relativeTo, x0, x1); + moveChildrenX = this.x.postApply(getWidget().getArea(), relativeTo, x0, x1); } if (coverHeight) { - moveChildrenY = this.y.postApply(this.parent.getArea(), relativeTo, y0, y1); + moveChildrenY = this.y.postApply(getWidget().getArea(), relativeTo, y0, y1); } // since the edges might have been moved closer to the widgets, the widgets should move back into it's original (absolute) position if (moveChildrenX != 0 || moveChildrenY != 0) { for (IWidget child : children) { Area area = child.getArea(); - IResizeable resizeable = child.resizer(); + ResizeNode resizeable = child.resizer(); if (resizeable.isXCalculated()) area.rx += moveChildrenX; if (resizeable.isYCalculated()) area.ry += moveChildrenY; } @@ -542,7 +235,7 @@ public boolean postResize(IGuiElement guiElement) { private void coverChildrenForLayout(ILayoutWidget layout, IWidget widget) { List children = widget.getChildren(); - Box padding = this.parent.getArea().getPadding(); + Box padding = getWidget().getArea().getPadding(); // first calculate the area the children span int x1 = Integer.MIN_VALUE, y1 = Integer.MIN_VALUE; int w = 0, h = 0; @@ -559,7 +252,7 @@ private void coverChildrenForLayout(ILayoutWidget layout, IWidget widget) { Box margin = area.getMargin(); IResizeable resizeable = child.resizer(); if (coverWidth) { - if (!child.flex().x.dependsOnParent()) { + if (!child.resizer().dependsOnParentX()) { hasIndependentChildX = true; if (resizeable.isWidthCalculated() && resizeable.isXCalculated()) { int s = area.requestedWidth() + padding.horizontal(); @@ -575,7 +268,7 @@ private void coverChildrenForLayout(ILayoutWidget layout, IWidget widget) { } if (coverHeight) { - if (!child.flex().y.dependsOnParent()) { + if (!child.resizer().dependsOnParentY()) { hasIndependentChildY = true; if (resizeable.isHeightCalculated() && resizeable.isYCalculated()) { int s = area.requestedHeight() + padding.vertical(); @@ -592,7 +285,7 @@ private void coverChildrenForLayout(ILayoutWidget layout, IWidget widget) { } if ((coverWidth && !hasIndependentChildX && !coverByDefaultSizeX) || (coverHeight && !hasIndependentChildY && !coverByDefaultSizeY)) { - GuiError.throwNew(this.parent, GuiError.Type.SIZING, "Can't cover children when all children depend on their parent!"); + GuiError.throwNew(getWidget(), GuiError.Type.SIZING, "Can't cover children when all children depend on their parent!"); return; } if (w == 0) w = withDefaultW; // only use default sizes, if no size is defined @@ -602,64 +295,355 @@ private void coverChildrenForLayout(ILayoutWidget layout, IWidget widget) { if (w > x1) x1 = w; if (h > y1) y1 = h; - Area relativeTo = getRelativeTo().getArea(); + Area relativeTo = getParent().getArea(); if (coverWidth) this.x.postApply(getArea(), relativeTo, 0, x1); if (coverHeight) this.y.postApply(getArea(), relativeTo, 0, y1); } private void coverChildrenForEmpty() { if (this.x.dependsOnChildren()) { - this.x.coverChildrenForEmpty(this.parent.getArea(), getRelativeTo().getArea()); + this.x.coverChildrenForEmpty(getWidget().getArea(), getParent().getArea()); } if (this.y.dependsOnChildren()) { - this.y.coverChildrenForEmpty(this.parent.getArea(), getRelativeTo().getArea()); + this.y.coverChildrenForEmpty(getWidget().getArea(), getParent().getArea()); } } @Override - public void applyPos(IGuiElement parent) { - Area relativeTo = getRelativeTo().getArea(); - Area area = parent.getArea(); + public void preApplyPos() { + IWidget widget = getWidget(); + Area relativeTo = getParent().getArea(); + Area area = widget.getArea(); // apply margin and padding if not done yet - this.x.applyMarginAndPaddingToPos(parent, area, relativeTo); - this.y.applyMarginAndPaddingToPos(parent, area, relativeTo); + this.x.applyMarginAndPaddingToPos(widget, area, relativeTo); + this.y.applyMarginAndPaddingToPos(widget, area, relativeTo); // after all widgets x, y, width and height have been calculated we can now calculate the absolute position area.applyPos(relativeTo.x, relativeTo.y); - Area parentArea = parent.getParentArea(); + } + + @Override + public void applyPos() { + IWidget widget = getWidget(); + if (widget instanceof IDelegatingWidget dw && dw.getDelegate() != null) { + super.postFullResize(); + return; + } + Area area = widget.getArea(); + // update rx and ry to be relative to the widget parent not the resize node parent + Area parentArea = widget.getParentArea(); area.rx = area.x - parentArea.x; area.ry = area.y - parentArea.y; - if (parent instanceof IVanillaSlot vanillaSlot && vanillaSlot.handleAsVanillaSlot()) { + if (widget instanceof IVanillaSlot vanillaSlot && vanillaSlot.handleAsVanillaSlot()) { // special treatment for minecraft slots Slot slot = vanillaSlot.getVanillaSlot(); - Area mainArea = parent.getScreen().getMainPanel().getArea(); + Area mainArea = widget.getScreen().getMainPanel().getArea(); // in vanilla uis the position is relative to the gui area and size is 16 x 16 // since our slots are 18 x 18 we need to offset by 1 - slot.xPos = parent.getArea().x - mainArea.x + 1; - slot.yPos = parent.getArea().y - mainArea.y + 1; + slot.xPos = widget.getArea().x - mainArea.x + 1; + slot.yPos = widget.getArea().y - mainArea.y + 1; } } + @Override + public void setChildrenResized(boolean resized) { + this.childrenResized = resized; + } + + @Override + public void setLayoutDone(boolean done) { + this.layoutResized = done; + } + + @Override + public void setResized(boolean x, boolean y, boolean w, boolean h) { + this.x.setResized(x, w); + this.y.setResized(y, h); + } + + @Override + public void setXMarginPaddingApplied(boolean b) { + this.x.setMarginPaddingApplied(b); + } + + @Override + public void setYMarginPaddingApplied(boolean b) { + this.y.setMarginPaddingApplied(b); + } + + @Override + public boolean hasYPos() { + return this.y.hasPos(); + } + + @Override + public boolean hasXPos() { + return this.x.hasPos(); + } + + @Override + public boolean hasHeight() { + return this.y.hasSize(); + } + + @Override + public boolean hasWidth() { + return this.x.hasSize(); + } + + @Override + public boolean hasStartPos(GuiAxis axis) { + return axis.isHorizontal() ? this.x.hasStart() : this.y.hasStart(); + } + + @Override + public boolean hasEndPos(GuiAxis axis) { + return axis.isHorizontal() ? this.x.hasEnd() : this.y.hasEnd(); + } + + @Override + public boolean dependsOnParentX() { + return this.x.dependsOnParent(); + } + + @Override + public boolean dependsOnParentY() { + return this.y.dependsOnParent(); + } + + @Override + public boolean dependsOnChildrenX() { + return this.x.dependsOnChildren(); + } + + @Override + public boolean dependsOnChildrenY() { + return this.y.dependsOnChildren(); + } + + public StandardResizer expanded() { + this.expanded = true; + scheduleResize(); + return this; + } + + @Override + public boolean isExpanded() { + return this.expanded; + } + + @Override + public boolean hasFixedSize() { + return this.x.hasFixedSize() && this.y.hasFixedSize(); + } + + @Override + public boolean isFullSize() { + if (!hasHeight() || !hasWidth()) return false; + return this.x.isFullSize() && this.y.isFullSize(); + } + + @Override + public StandardResizer relative(ResizeNode resizeNode) { + setParentOverride(resizeNode); + this.relativeToScreen = false; + return this; + } + + @Override + public StandardResizer relativeToParent() { + setParentOverride(null); + this.relativeToScreen = false; + return this; + } + + @Override + public StandardResizer relativeToScreen() { + this.relativeToScreen = true; + return this; + } + + @Override + public StandardResizer coverChildren() { + this.x.setCoverChildren(true, getWidget()); + this.y.setCoverChildren(true, getWidget()); + return this; + } + + @Override + public StandardResizer coverChildrenWidth() { + this.x.setCoverChildren(true, getWidget()); + return this; + } + + @Override + public StandardResizer coverChildrenHeight() { + this.y.setCoverChildren(true, getWidget()); + return this; + } + + @ApiStatus.Internal + public StandardResizer left(float x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getLeft(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer left(DoubleSupplier x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getLeft(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer right(float x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getRight(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer right(DoubleSupplier x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getRight(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer top(float y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getTop(), y, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer top(DoubleSupplier y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getTop(), y, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer bottom(float y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getBottom(), y, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public StandardResizer bottom(DoubleSupplier y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getBottom(), y, offset, anchor, measure, autoAnchor); + } + + private StandardResizer unit(Unit u, float val, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + u.setValue(val); + u.setMeasure(measure); + u.setOffset(offset); + u.setAnchor(anchor); + u.setAutoAnchor(autoAnchor); + scheduleResize(); + return this; + } + + private StandardResizer unit(Unit u, DoubleSupplier val, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + u.setValue(val); + u.setMeasure(measure); + u.setOffset(offset); + u.setAnchor(anchor); + u.setAutoAnchor(autoAnchor); + scheduleResize(); + return this; + } + + @ApiStatus.Internal + public StandardResizer width(float val, int offset, Unit.Measure measure) { + return unitSize(getWidth(), val, offset, measure); + } + + @ApiStatus.Internal + public StandardResizer width(DoubleSupplier val, int offset, Unit.Measure measure) { + return unitSize(getWidth(), val, offset, measure); + } + + @ApiStatus.Internal + public StandardResizer height(float val, int offset, Unit.Measure measure) { + return unitSize(getHeight(), val, offset, measure); + } + + @ApiStatus.Internal + public StandardResizer height(DoubleSupplier val, int offset, Unit.Measure measure) { + return unitSize(getHeight(), val, offset, measure); + } + + private StandardResizer unitSize(Unit u, float val, int offset, Unit.Measure measure) { + u.setValue(val); + u.setMeasure(measure); + u.setOffset(offset); + scheduleResize(); + return this; + } + + private StandardResizer unitSize(Unit u, DoubleSupplier val, int offset, Unit.Measure measure) { + u.setValue(val); + u.setMeasure(measure); + u.setOffset(offset); + scheduleResize(); + return this; + } + + public StandardResizer anchorLeft(float val) { + getLeft().setAnchor(val); + getLeft().setAutoAnchor(false); + scheduleResize(); + return this; + } + + public StandardResizer anchorRight(float val) { + getRight().setAnchor(1 - val); + getRight().setAutoAnchor(false); + scheduleResize(); + return this; + } + + public StandardResizer anchorTop(float val) { + getTop().setAnchor(val); + getTop().setAutoAnchor(false); + scheduleResize(); + return this; + } + + public StandardResizer anchorBottom(float val) { + getBottom().setAnchor(1 - val); + getBottom().setAutoAnchor(false); + scheduleResize(); + return this; + } + + public StandardResizer anchor(Alignment alignment) { + if (this.x.hasStart() || !this.x.hasEnd()) { + anchorLeft(alignment.x); + } else if (this.x.hasEnd()) { + anchorRight(alignment.x); + } + if (this.y.hasStart() || !this.y.hasEnd()) { + anchorTop(alignment.y); + } else if (this.y.hasEnd()) { + anchorBottom(alignment.y); + } + return this; + } + + public void setUnit(Unit unit, GuiAxis axis, Unit.State pos) { + (axis.isHorizontal() ? this.x : this.y).setUnit(unit, pos); + } + private Unit getLeft() { - return this.x.getStart(this.parent); + return this.x.getStart(getWidget()); } private Unit getRight() { - return this.x.getEnd(this.parent); + return this.x.getEnd(getWidget()); } private Unit getTop() { - return this.y.getStart(this.parent); + return this.y.getStart(getWidget()); } private Unit getBottom() { - return this.y.getEnd(this.parent); + return this.y.getEnd(getWidget()); } private Unit getWidth() { - return this.x.getSize(this.parent); + return this.x.getSize(getWidget()); } private Unit getHeight() { - return this.y.getSize(this.parent); + return this.y.getSize(getWidget()); } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/StaticResizer.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/StaticResizer.java new file mode 100644 index 000000000..48500a050 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/StaticResizer.java @@ -0,0 +1,128 @@ +package com.cleanroommc.modularui.widget.sizer; + +import com.cleanroommc.modularui.api.GuiAxis; + +public abstract class StaticResizer extends ResizeNode { + + private boolean childrenCalculated = false; + + public StaticResizer() {} + + @Override + public void initResizing(boolean onOpen) { + super.initResizing(onOpen); + setChildrenResized(false); + } + + @Override + public boolean isXCalculated() { + return true; + } + + @Override + public boolean isYCalculated() { + return true; + } + + @Override + public boolean isWidthCalculated() { + return true; + } + + @Override + public boolean isHeightCalculated() { + return true; + } + + @Override + public boolean areChildrenCalculated() { + return this.childrenCalculated; + } + + @Override + public boolean isLayoutDone() { + return true; + } + + @Override + public boolean canRelayout(boolean isParentLayout) { + return false; + } + + @Override + public boolean isXMarginPaddingApplied() { + return true; + } + + @Override + public boolean isYMarginPaddingApplied() { + return true; + } + + @Override + public boolean resize(boolean isParentLayout) { + return true; + } + + @Override + public boolean postResize() { + return true; + } + + @Override + public void setChildrenResized(boolean resized) { + this.childrenCalculated = resized; + } + + @Override + public void setLayoutDone(boolean done) {} + + @Override + public void setResized(boolean x, boolean y, boolean w, boolean h) {} + + @Override + public void setXMarginPaddingApplied(boolean b) {} + + @Override + public void setYMarginPaddingApplied(boolean b) {} + + @Override + public boolean hasYPos() { + return true; + } + + @Override + public boolean hasXPos() { + return true; + } + + @Override + public boolean hasHeight() { + return true; + } + + @Override + public boolean hasWidth() { + return true; + } + + @Override + public boolean hasStartPos(GuiAxis axis) { + return true; + } + + @Override + public boolean hasEndPos(GuiAxis axis) { + return false; + } + + @Override + public boolean hasFixedSize() { + return true; + } + + @Override + public boolean isFullSize() { + return false; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widget/sizer/WidgetResizeNode.java b/src/main/java/com/cleanroommc/modularui/widget/sizer/WidgetResizeNode.java new file mode 100644 index 000000000..e3ee4441a --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widget/sizer/WidgetResizeNode.java @@ -0,0 +1,73 @@ +package com.cleanroommc.modularui.widget.sizer; + +import com.cleanroommc.modularui.api.layout.ILayoutWidget; +import com.cleanroommc.modularui.api.widget.IWidget; + +import java.util.Objects; + +public abstract class WidgetResizeNode extends ResizeNode { + + private final IWidget widget; + + protected WidgetResizeNode(IWidget widget) { + this.widget = Objects.requireNonNull(widget); + } + + public IWidget getWidget() { + return widget; + } + + @Override + public Area getArea() { + return widget.getArea(); + } + + @Override + public void initResizing(boolean onOpen) { + super.initResizing(onOpen); + this.widget.beforeResize(onOpen); + } + + @Override + public void onResized() { + super.onResized(); + this.widget.onResized(); + } + + @Override + public void postFullResize() { + super.postFullResize(); + this.widget.postResize(); + } + + @Override + public boolean isLayout() { + return this.widget instanceof ILayoutWidget; + } + + @Override + public boolean layoutChildren() { + if (this.widget instanceof ILayoutWidget layoutWidget) { + return layoutWidget.layoutWidgets(); + } + return true; + } + + @Override + public boolean postLayoutChildren() { + if (this.widget instanceof ILayoutWidget layoutWidget) { + return layoutWidget.postLayoutWidgets(); + } + return true; + } + + @Override + public String getDebugDisplayName() { + return "widget '" + this.widget + "' of screen '" + this.widget.getScreen() + "'"; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + this.widget + ")"; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java index 133e7e376..6b3dd351e 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/AbstractCycleButtonWidget.java @@ -9,12 +9,15 @@ import com.cleanroommc.modularui.api.value.IEnumValue; import com.cleanroommc.modularui.api.value.IIntValue; import com.cleanroommc.modularui.api.value.ISyncOrValue; +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.api.widget.Interactable; import com.cleanroommc.modularui.drawable.UITexture; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.theme.WidgetThemeEntry; import com.cleanroommc.modularui.utils.Alignment; import com.cleanroommc.modularui.value.IntValue; +import com.cleanroommc.modularui.value.sync.SyncHandler; +import com.cleanroommc.modularui.widget.SingleChildWidget; import com.cleanroommc.modularui.widget.Widget; import org.jetbrains.annotations.NotNull; @@ -23,7 +26,7 @@ import java.util.Arrays; import java.util.function.Consumer; -public class AbstractCycleButtonWidget> extends Widget implements Interactable { +public class AbstractCycleButtonWidget> extends SingleChildWidget implements Interactable { private static final RichTooltip[] EMPTY_TOOLTIP = new RichTooltip[0]; @@ -37,12 +40,15 @@ public class AbstractCycleButtonWidget> e protected IDrawable[] overlay = null; protected IDrawable[] hoverOverlay = null; protected RichTooltip[] tooltip = EMPTY_TOOLTIP; + protected IWidget[] stateChildren = null; + protected IWidget fallbackChild = null; @Override public void onInit() { if (this.intValue == null) { this.intValue = new IntValue(0); } + updateChild(getState()); } @Override @@ -81,6 +87,8 @@ private void setStateCount(int stateCount) { this.overlay = checkArray(this.overlay, stateCount); this.hoverBackground = checkArray(this.hoverBackground, stateCount); this.hoverOverlay = checkArray(this.hoverOverlay, stateCount); + if (this.stateChildren == null) this.stateChildren = new IWidget[stateCount]; + else if (this.stateChildren.length < stateCount) this.stateChildren = Arrays.copyOf(this.stateChildren, stateCount); } protected void expectCount() { @@ -126,6 +134,7 @@ public void setState(int state, boolean setSource) { if (state < 0 || state >= this.stateCount) { throw new IndexOutOfBoundsException("CycleButton state out of bounds"); } + updateChild(state); if (setSource) { this.intValue.setIntValue(state); } @@ -133,6 +142,15 @@ public void setState(int state, boolean setSource) { markTooltipDirty(); } + private void updateChild(int state) { + IWidget child = this.stateChildren != null && this.stateChildren.length > state ? this.stateChildren[state] : null; + if (child != null) { + child(child); + } else if (getChild() != this.fallbackChild) { + child(this.fallbackChild); + } + } + @Override public @NotNull Result onMousePressed(int mouseButton) { switch (mouseButton) { @@ -223,11 +241,39 @@ public W disableHoverOverlay() { return getThis(); } + @Override + public W invisible() { + if (this.background != null) { + Arrays.fill(this.background, IDrawable.EMPTY); + } + if (getBackground() == null) { + super.background(IDrawable.EMPTY); + } + return disableHoverBackground(); + } + protected W value(IIntValue value) { setSyncOrValue(ISyncOrValue.orEmpty(value)); return getThis(); } + @Override + public W child(IWidget child) { + this.fallbackChild = child; + return super.child(child); + } + + public W stateChild(int state, IWidget child) { + updateStateCount(state, false); + if (this.stateChildren == null) { + this.stateChildren = new IWidget[state + 1]; + } else if (this.stateChildren.length < state + 1) { + this.stateChildren = Arrays.copyOf(this.stateChildren, state + 1); + } + this.stateChildren[state] = child; + return getThis(); + } + /** * Sets the state dependent background. The images should be vertically stacked images from top to bottom * Note: The length must be already set! diff --git a/src/main/java/com/cleanroommc/modularui/widgets/CategoryList.java b/src/main/java/com/cleanroommc/modularui/widgets/CategoryList.java index 9b714badb..15e338276 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/CategoryList.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/CategoryList.java @@ -139,7 +139,8 @@ public void onChildAdd(IWidget child) { private void updateHeight() { layoutWidgets(); - WidgetTree.applyPos(this); + WidgetTree.preApplyPos(resizer()); + WidgetTree.applyPos(resizer()); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java b/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java index c77de8fcd..d0e0e2f15 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java @@ -49,6 +49,12 @@ public void onInit() { @Override public void beforeResize(boolean onOpen) { super.beforeResize(onOpen); + if (resizer().getChildren().isEmpty() || resizer().getChildren().size() > 2) + throw new IllegalStateException("Invalid Expandable children size"); + if (resizer().getChildren().size() > 1) { + resizer().getChildren().remove(1); + } + resizer().getChildren().set(0, this.expanded ? this.expandedView.resizer() : this.normalView.resizer()); this.currentChildren = Collections.singletonList(this.expanded ? this.expandedView : this.normalView); } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ListWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/ListWidget.java index b21797aae..cf7c8edb3 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ListWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ListWidget.java @@ -58,7 +58,7 @@ public void onInit() { public void beforeResize(boolean onOpen) { super.beforeResize(onOpen); if (this.mainAxisMaxSize != null) { - flex().setUnit(this.mainAxisMaxSize, getAxis(), Unit.State.SIZE); + resizer().setUnit(this.mainAxisMaxSize, getAxis(), Unit.State.SIZE); } } @@ -99,7 +99,7 @@ public boolean layoutWidgets() { widget.resizer().updateResized(); continue; } - if (widget.flex().hasPos(axis)) { + if (widget.resizer().hasPos(axis)) { widget.resizer().updateResized(); // this is required when the widget has a pos on the main axis, but not on the cross axis continue; } @@ -112,9 +112,9 @@ public boolean layoutWidgets() { widget.resizer().setMarginPaddingApplied(true); this.separatorPositions.add(p); p += separatorSize; - if (isValid()) { - widget.flex().applyPos(widget); - } + /*if (isValid()) { + widget.resizer().applyPos(); + }*/ } int size = p + getArea().getPadding().getEnd(axis); getScrollData().setScrollSize(size); diff --git a/src/main/java/com/cleanroommc/modularui/widgets/PopupMenu.java b/src/main/java/com/cleanroommc/modularui/widgets/PopupMenu.java deleted file mode 100644 index f8b39b1d4..000000000 --- a/src/main/java/com/cleanroommc/modularui/widgets/PopupMenu.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.cleanroommc.modularui.widgets; - -import com.cleanroommc.modularui.api.widget.IWidget; -import com.cleanroommc.modularui.widget.Widget; - -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.List; - -public class PopupMenu> extends Widget { - - private final MenuWrapper menu; - private final List children; - - public PopupMenu(IWidget child) { - this.menu = new MenuWrapper(child); - child.flex().relative(this.getArea()); - this.menu.setEnabled(false); - this.children = Collections.singletonList(this.menu); - } - - @NotNull - @Override - public List getChildren() { - return this.children; - } - - @Override - public void onMouseStartHover() { - super.onMouseStartHover(); - this.menu.setEnabled(true); - this.menu.mightClose = false; - } - - @Override - public void onMouseEndHover() { - super.onMouseEndHover(); - this.menu.mightClose = true; - } - - public static class MenuWrapper extends Widget { - - private final IWidget child; - private final List children; - private boolean mightClose = false; - - private MenuWrapper(IWidget child) { - this.child = child; - this.children = Collections.singletonList(child); - flex().coverChildren().cancelMovementX().cancelMovementY(); - } - - @Override - public @NotNull List getChildren() { - return this.children; - } - - @Override - public void onUpdate() { - super.onUpdate(); - if (this.mightClose && !isBelowMouse()) { - setEnabled(false); - } - } - - public void setMightClose(boolean mightClose) { - this.mightClose = mightClose; - } - } -} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java index 9ee469d4d..ab7ff1f77 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SliderWidget.java @@ -94,7 +94,7 @@ public void drawBackground(ModularGuiContext context, WidgetThemeEntry widget @Override public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { if (this.handleDrawable != null) { - this.handleDrawable.draw(context, this.sliderArea, context.getTheme().getButtonTheme().getTheme()); + this.handleDrawable.draw(context, this.sliderArea, getPanel().getTheme().getButtonTheme().getTheme()); } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java index 6ec403d4e..0de7d5f8d 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SlotGroupWidget.java @@ -248,7 +248,7 @@ public SlotGroupWidget build() { x += 18; continue; } - widget.flex().left(x).top(y); + widget.resizer().left(x).top(y); slotGroupWidget.child(widget); if (this.syncKey != null && widget instanceof ISynced synced) { synced.syncHandler(this.syncKey, syncId++); @@ -259,7 +259,7 @@ public SlotGroupWidget build() { y += 18; x = 0; } - slotGroupWidget.flex().size(maxWidth, this.matrix.size() * 18); + slotGroupWidget.resizer().size(maxWidth, this.matrix.size() * 18); return slotGroupWidget; } } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java index 532126f16..07fa0c4a4 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java @@ -2,13 +2,12 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.animation.Animator; -import com.cleanroommc.modularui.api.widget.IGuiElement; import com.cleanroommc.modularui.api.widget.IValueWidget; import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.drawable.GuiTextures; +import com.cleanroommc.modularui.screen.viewport.LocatedWidget; import com.cleanroommc.modularui.utils.ObjectList; import com.cleanroommc.modularui.widget.DraggableWidget; -import com.cleanroommc.modularui.widget.WidgetTree; import com.cleanroommc.modularui.widget.sizer.Area; import org.jetbrains.annotations.NotNull; @@ -157,14 +156,14 @@ public static class Item extends DraggableWidget> implements IValueWi private final T value; private List children; - private Predicate dropPredicate; + private Predicate dropPredicate; private SortableListWidget listWidget; private int index = -1; private final int movingFrom = -1; public Item(T value) { this.value = value; - flex().widthRel(1f).height(18); + resizer().widthRel(1f).height(18); background(GuiTextures.BUTTON_CLEAN); } @@ -183,18 +182,18 @@ public List getChildren() { } @Override - public boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + public boolean canDropHere(int x, int y, @Nullable IWidget widget) { return this.dropPredicate == null || this.dropPredicate.test(widget); } @Override public void onDrag(int mouseButton, long timeSinceLastClick) { super.onDrag(mouseButton, timeSinceLastClick); - // TODO this kind of assumes the hovered is in bounds of the parent Item, which may not be true. - IWidget hovered = getContext().getTopHovered(); - SortableListWidget.Item item = WidgetTree.findParent(hovered, Item.class); - if (item != null && item != this && item.listWidget == this.listWidget) { - this.listWidget.moveTo(this.index, item.index); + for (LocatedWidget hovering : getPanel().getAllHoveringList(false)) { + if (hovering.getElement() instanceof SortableListWidget.Item item && item != this && item.listWidget == this.listWidget) { + this.listWidget.moveTo(this.index, item.index); + break; + } } } @@ -225,7 +224,7 @@ public Item child(Function, IWidget> widgetCreator) { return child(widgetCreator.apply(this)); } - public Item dropPredicate(Predicate dropPredicate) { + public Item dropPredicate(Predicate dropPredicate) { this.dropPredicate = dropPredicate; return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/TextWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/TextWidget.java index 6fc0dca79..9d390e8d9 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/TextWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/TextWidget.java @@ -62,7 +62,7 @@ protected String checkString() { protected void onTextChanged(String newText) { // scheduling it would resize it on next frame, but we need it now - WidgetTree.resizeInternal(this, false); + WidgetTree.resizeInternal(resizer(), false); } private TextRenderer simulate(float maxWidth) { diff --git a/src/main/java/com/cleanroommc/modularui/widgets/ToggleButton.java b/src/main/java/com/cleanroommc/modularui/widgets/ToggleButton.java index 945884578..3af614c56 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/ToggleButton.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/ToggleButton.java @@ -3,6 +3,7 @@ import com.cleanroommc.modularui.api.ITheme; import com.cleanroommc.modularui.api.drawable.IDrawable; import com.cleanroommc.modularui.api.value.IBoolValue; +import com.cleanroommc.modularui.api.widget.IWidget; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.theme.SelectableTheme; import com.cleanroommc.modularui.theme.WidgetTheme; @@ -101,6 +102,10 @@ public ToggleButton invertSelected(boolean invert) { return getThis(); } + public ToggleButton child(boolean selected, IWidget widget) { + return stateChild(selected ? 1 : 0, widget); + } + public boolean invertSelected() { return this.invert; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/TransformWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/TransformWidget.java index 8b47f718e..d6d0901da 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/TransformWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/TransformWidget.java @@ -2,14 +2,14 @@ import com.cleanroommc.modularui.api.layout.IViewportStack; import com.cleanroommc.modularui.api.widget.IWidget; -import com.cleanroommc.modularui.widget.DelegatingSingleChildWidget; +import com.cleanroommc.modularui.widget.DelegatingWidget; import org.joml.Matrix4f; import org.joml.Vector3f; import java.util.function.Consumer; -public class TransformWidget extends DelegatingSingleChildWidget { +public class TransformWidget extends DelegatingWidget { private static final Vector3f sharedVec = new Vector3f(); @@ -17,10 +17,8 @@ public class TransformWidget extends DelegatingSingleChildWidget transform; - public TransformWidget() {} - public TransformWidget(IWidget child) { - child(child); + super(child); } @Override diff --git a/src/main/java/com/cleanroommc/modularui/widgets/layout/Flow.java b/src/main/java/com/cleanroommc/modularui/widgets/layout/Flow.java index 8e92b6729..ace8e1f55 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/layout/Flow.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/layout/Flow.java @@ -7,11 +7,12 @@ import com.cleanroommc.modularui.utils.ReversedList; import com.cleanroommc.modularui.widget.ParentWidget; import com.cleanroommc.modularui.widget.sizer.Box; +import com.cleanroommc.modularui.widget.sizer.ExpanderResizer; import java.util.List; import java.util.function.IntFunction; -public class Flow extends ParentWidget implements ILayoutWidget, IExpander { +public class Flow extends ParentWidget implements ILayoutWidget { public static Flow row() { return new Flow(GuiAxis.X); @@ -49,6 +50,7 @@ public static Flow column() { public Flow(GuiAxis axis) { this.axis = axis; + resizer(new ExpanderResizer(this, axis)); sizeRel(1f, 1f); } @@ -67,8 +69,8 @@ public int getDefaultMainAxisSize() { GuiAxis axis = this.axis; int total = getArea().getPadding().getTotal(axis); for (IWidget widget : getChildren()) { - if (shouldIgnoreChildSize(widget) || widget.flex().hasPos(axis)) continue; - if (widget.flex().isExpanded() || !widget.resizer().isSizeCalculated(axis)) { + if (shouldIgnoreChildSize(widget) || widget.resizer().hasPos(axis)) continue; + if (widget.resizer().isExpanded() || !widget.resizer().isSizeCalculated(axis)) { total += axis.isHorizontal() ? widget.getDefaultWidth() : widget.getDefaultHeight(); } else { total += widget.getArea().getSize(axis); @@ -103,7 +105,7 @@ public boolean layoutWidgets() { final int size = getArea().getSize(axis) - padding.getTotal(this.axis); Alignment.MainAxis maa = this.maa; if (!hasSize && maa != Alignment.MainAxis.START) { - if (flex().dependsOnChildren(this.axis)) { + if (resizer().dependsOnChildren(this.axis)) { // if this flow covers the children, we can assume start maa = Alignment.MainAxis.START; } else { @@ -123,9 +125,9 @@ public boolean layoutWidgets() { // ignore disabled child if configured as such if (shouldIgnoreChildSize(widget)) continue; // exclude children whose position of main axis is fixed - if (widget.flex().hasPos(this.axis)) continue; + if (widget.resizer().hasPos(this.axis)) continue; amount++; - if (widget.flex().isExpanded()) { + if (widget.resizer().isExpanded()) { expandedAmount++; childrenSize += widget.getArea().getMargin().getTotal(this.axis); continue; @@ -155,8 +157,8 @@ public boolean layoutWidgets() { // ignore disabled child if configured as such if (shouldIgnoreChildSize(widget)) continue; // exclude children whose position of main axis is fixed - if (widget.flex().hasPos(this.axis)) continue; - if (widget.flex().isExpanded()) { + if (widget.resizer().hasPos(this.axis)) continue; + if (widget.resizer().isExpanded()) { widget.getArea().setSize(this.axis, newSize); widget.resizer().setSizeResized(this.axis, true); } @@ -181,7 +183,7 @@ public boolean layoutWidgets() { continue; } // exclude children whose position of main axis is fixed - if (widget.flex().hasPos(this.axis)) { + if (widget.resizer().hasPos(this.axis)) { widget.resizer().updateResized(); // this is required when the widget has a pos on the main axis, but not on the cross axis continue; } @@ -215,10 +217,10 @@ public static boolean layoutCrossAxisListLike(IWidget parent, GuiAxis axis, Alig List childrenList = reverseLayout ? new ReversedList<>(parent.getChildren()) : parent.getChildren(); for (IWidget widget : childrenList) { // exclude children whose position of main axis is fixed - if (widget.flex().hasPos(axis)) continue; + if (widget.resizer().hasPos(axis)) continue; Box margin = widget.getArea().getMargin(); // don't align auto positioned children in cross axis - if (!widget.flex().hasPos(other) && widget.resizer().isSizeCalculated(other)) { + if (!widget.resizer().hasPos(other) && widget.resizer().isSizeCalculated(other)) { int crossAxisPos = margin.getStart(other) + padding.getStart(other); if (hasWidth) { if (caa == Alignment.CrossAxis.CENTER) { @@ -232,10 +234,10 @@ public static boolean layoutCrossAxisListLike(IWidget parent, GuiAxis axis, Alig widget.resizer().setPosResized(other, true); widget.resizer().setMarginPaddingApplied(other, true); } - if (parent.isValid()) { + /*if (parent.isValid()) { // we changed rel pos, but we need to calculate the new absolute pos and other stuff - widget.flex().applyPos(widget); - } + widget.resizer().applyPos(); + }*/ } return true; } @@ -322,11 +324,6 @@ public GuiAxis getAxis() { return axis; } - @Override - public GuiAxis getExpandAxis() { - return this.axis; - } - @Override protected String getTypeName() { return this.axis.isHorizontal() ? "Row" : "Column"; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/menu/AbstractMenuButton.java b/src/main/java/com/cleanroommc/modularui/widgets/menu/AbstractMenuButton.java new file mode 100644 index 000000000..1bc4bfd5b --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/menu/AbstractMenuButton.java @@ -0,0 +1,226 @@ +package com.cleanroommc.modularui.widgets.menu; + +import com.cleanroommc.modularui.api.IPanelHandler; +import com.cleanroommc.modularui.api.ITheme; +import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.api.widget.Interactable; +import com.cleanroommc.modularui.theme.WidgetThemeEntry; +import com.cleanroommc.modularui.widget.Widget; +import com.cleanroommc.modularui.widget.WidgetTree; +import com.cleanroommc.modularui.widget.sizer.StandardResizer; + +import net.minecraft.util.text.TextFormatting; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * This is the base class for a button that can open a floating widget by clicking or hovering the button. In ModularUI this is used for + * context menus and dropdown menus. When the menu is opened a new panel is created and the {@link Menu} widget is added. If that menu + * contains another one of these menu buttons, it will be added to that panel. + * + * @param type of this widget + */ +public abstract class AbstractMenuButton> extends Widget implements IMenuPart, Interactable { + + /** + * The general direction where the menu will be opened. This is just a shortcut to standard resizer calls. + * If this is null you can customize the position yourself. + */ + protected Direction direction = Direction.DOWN; + /** + * If this is true, the menu can be opened when the mouse hovers this button. The menu will automatically close, when the button AND + * all widgets in the menus tree are no longer hovered. + */ + protected boolean openOnHover = true; + + /** + * The current menu widget. The menu will be created with {@link #createMenu()} if the menu is null when it's needed. If the method + * also returns a null value, a default menu is created. The menu can be set at any time with {@link #setMenu(Menu)}. + */ + private Menu menu; + private boolean open, softOpen; // state, soft means opened by hovering + private IPanelHandler panelHandler; + private final String panelName; + + /** + * @param panelName the name for the panel that may be created when opening the menu + */ + public AbstractMenuButton(String panelName) { + this.panelName = Objects.requireNonNull(panelName); + name(panelName); + } + + /** + * @return true if the menu is currently open (soft or hard) + */ + public boolean isOpen() { + return open; + } + + /** + * @return true if the menu is currently soft open (opened by hovering) + */ + protected boolean isSoftOpen() { + return softOpen; + } + + protected void toggleMenu(boolean soft) { + if (this.open) { + if (this.softOpen) { + if (soft) { + closeMenu(true); + } else { + this.softOpen = false; + } + } else if (!soft) { + if (this.openOnHover) { + this.softOpen = true; + } else { + closeMenu(false); + } + } + } else { + openMenu(soft); + } + } + + protected void openMenu(boolean soft) { + if (this.open) { + if (this.softOpen && !soft) { + this.softOpen = false; + } + return; + } + if (getPanel() instanceof MenuPanel menuPanel) { + menuPanel.openSubMenu(getMenu()); + } else { + getPanelHandler().openPanel(); + } + this.open = true; + this.softOpen = soft; + } + + protected void closeMenu(boolean soft) { + if (!this.open || (!this.softOpen && soft)) return; + if (getPanel() instanceof MenuPanel menuPanel) { + menuPanel.remove(getMenu()); + } else { + getPanelHandler().closePanel(); + } + this.open = false; + this.softOpen = false; + } + + protected Menu getMenu() { + if (this.menu == null) { + this.menu = createMenu(); + if (this.menu == null) { + this.menu = new Menu<>() + .child(IKey.str("No Menu supplied") + .style(TextFormatting.RED) + .asWidget() + .center()) + .widthRel(1f) + .height(16); + if (this.direction == null) Direction.DOWN.positioner.accept(this.menu.resizer()); + } + } + if (!this.menu.resizer().hasParentOverride()) { + this.menu.resizer().relative(this); + } + if (this.direction != null) { + this.direction.positioner.accept(this.menu.resizer()); + } + this.menu.setMenuSource(this); + return this.menu; + } + + protected void setMenu(Menu menu) { + this.menu = menu; + } + + protected abstract Menu createMenu(); + + private IPanelHandler getPanelHandler() { + if (this.panelHandler == null) { + this.panelHandler = IPanelHandler.simple(getPanel(), (parentPanel, player) -> new MenuPanel(this.panelName, getMenu()), true); + } + return this.panelHandler; + } + + @Override + public @NotNull Result onMousePressed(int mouseButton) { + if (!this.open) { + forEachSiblingMenuButton(w -> { + w.closeMenu(false); + return true; + }); + } + toggleMenu(false); + return Result.SUCCESS; + } + + @Override + public void onMouseEnterArea() { + super.onMouseEnterArea(); + if (this.openOnHover && forEachSiblingMenuButton(mb -> !mb.open || mb.softOpen)) { + openMenu(true); + } + } + + protected boolean forEachSiblingMenuButton(Predicate> test) { + Menu menuParent = WidgetTree.findParent(this, Menu.class); + if (menuParent != null) { + return WidgetTree.foreachChild(menuParent, w -> !(w instanceof AbstractMenuButton mb) || mb == this || test.test(mb), false); + } + return true; + } + + @Override + public void onMouseLeaveArea() { + super.onMouseLeaveArea(); + checkClose(); + } + + protected void checkClose() { + if (this.openOnHover && !isSelfOrChildHovered()) { + closeMenu(true); + Menu menuParent = WidgetTree.findParent(this, Menu.class); + if (menuParent != null) { + menuParent.checkClose(); + } + } + } + + @Override + public boolean isSelfOrChildHovered() { + if (isBelowMouse()) return true; + if (!isOpen() || this.menu == null) return false; + return this.menu.isSelfOrChildHovered(); + } + + @Override + protected WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { + return theme.getButtonTheme(); + } + + public enum Direction { + UP(flex -> flex.bottomRel(1f)), + DOWN(flex -> flex.topRel(1f)), + LEFT_UP(flex -> flex.rightRel(1f).bottom(0)), + LEFT_DOWN(flex -> flex.rightRel(1f).top(0)), + RIGHT_UP(flex -> flex.leftRel(1f).bottom(0)), + RIGHT_DOWN(flex -> flex.leftRel(1f).top(0)), + UNDEFINED(flex -> {}); + + private final Consumer positioner; + + Direction(Consumer positioner) { + this.positioner = positioner; + } + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/menu/ContextMenuButton.java b/src/main/java/com/cleanroommc/modularui/widgets/menu/ContextMenuButton.java new file mode 100644 index 000000000..496323310 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/menu/ContextMenuButton.java @@ -0,0 +1,155 @@ +package com.cleanroommc.modularui.widgets.menu; + +import com.cleanroommc.modularui.api.widget.IPositioned; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.widgets.ListWidget; + +import java.util.function.Consumer; + +/** + * A button that opens a context menu on click or hover. + * + * @param type of this widget + */ +public class ContextMenuButton> extends AbstractMenuButton { + + /** + * @param panelName the panel name that the menu may create + */ + public ContextMenuButton(String panelName) { + super(panelName); + this.openOnHover = true; + } + + @Override + protected Menu createMenu() { + // menu is created by the user with the setter + return null; + } + + /** + * Sets the menu widget. The menu will by default be relative to this button. + * It is common to use {@link IPositioned#widthRel(float)} and {@link IPositioned#coverChildrenHeight()}. + * The {@link #direction(Direction)} will handle the position of the menu, but can be customized if {@link #openCustom()} is used. + * + * @param menu displayed menu + * @return this + */ + public W menu(Menu menu) { + setMenu(menu); + return getThis(); + } + + /** + * This is a shortcut that is meant to be used when a simple list of options should be displayed. It is recommended to call + * {@link ListWidget#maxSize(int)} to limit the size. This is method is not suited for any customization on the actual menu widget. + * + * @param builder list builder which is called exactly once + * @return this + */ + public W menuList(Consumer> builder) { + ListWidget l = new ListWidget<>().widthRel(1f); + builder.accept(l); + return menu(new Menu<>() + .widthRel(1f) + .coverChildrenHeight() + .child(l)); + } + + /** + * Sets the general direction in which the menu should open. This is just a shortcut to {@link IPositioned} position calls. + * Use {@link #openCustom()} if none of the predefined options suit your needs. You can then position the menu yourself. + * + * @param direction general direction to open the menu in + * @return this + */ + public W direction(Direction direction) { + this.direction = direction; + return getThis(); + } + + /** + * Sets this button to require a click to open the menu. Hovering this button will not open it. + * + * @return this + */ + public W requiresClick() { + return openOnHover(false); + } + + /** + * Sets whether the menu should be opened when this button is hovered by the mouse. + * + * @param openOnHover true if the menu can be opened by hover + * @return this + */ + public W openOnHover(boolean openOnHover) { + this.openOnHover = openOnHover; + return getThis(); + } + + /** + * Sets the menu to open in the "up" direction. This does not set a horizontal position. + * This is best used with {@link IPositioned#widthRel(float) IPositioned.widthRel(1f)} + * + * @return this + */ + public W openUp() { + return direction(Direction.UP); + } + + /** + * Sets the menu to open in the "down" direction. This does not set a horizontal position. + * This is best used with {@link IPositioned#widthRel(float) IPositioned.widthRel(1f)} + * + * @return this + */ + public W openDown() { + return direction(Direction.DOWN); + } + + /** + * Sets the menu to open in the "left and up" direction. + * + * @return this + */ + public W openLeftUp() { + return direction(Direction.LEFT_UP); + } + + /** + * Sets the menu to open in the "left and down" direction. + * + * @return this + */ + public W openLeftDown() { + return direction(Direction.LEFT_DOWN); + } + + /** + * Sets the menu to open in the "right and up" direction. + * + * @return this + */ + public W openRightUp() { + return direction(Direction.RIGHT_UP); + } + + /** + * Sets the menu to open in the "right and down" direction. + * + * @return this + */ + public W openRightDown() { + return direction(Direction.RIGHT_DOWN); + } + + /** + * Sets the menu to open in no specified direction. The position of the menu must be set manually, or it is left at 0,0. + * + * @return this + */ + public W openCustom() { + return direction(Direction.UNDEFINED); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/menu/DropdownWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/menu/DropdownWidget.java new file mode 100644 index 000000000..fc984c452 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/menu/DropdownWidget.java @@ -0,0 +1,222 @@ +package com.cleanroommc.modularui.widgets.menu; + +import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.api.value.ISyncOrValue; +import com.cleanroommc.modularui.api.value.IValue; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.utils.MutableSingletonList; +import com.cleanroommc.modularui.widgets.ButtonWidget; +import com.cleanroommc.modularui.widgets.ListWidget; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A button which displays a list of options when clicked. When an option is clicked, the menu closes and the button displays that selected + * option. To use it, supply the appropriate value type you want to use to the constructor. This is used to validate synced values. + * Add options that can be selected with {@link #option(Object)}, {@link #options(Iterable)} or {@link #options(Object[])}. + * Finally set a function that converts an option to a widget using {@link #optionToWidget(ToWidget)}. When a value is selected, it will + * sync to any set value handler. + * + * @param type of the values used + * @param type of this widget + */ +public class DropdownWidget> extends AbstractMenuButton { + + private final Class valueType; + private final MutableSingletonList selected = new MutableSingletonList<>(); + private final List values = new ArrayList<>(); + private IValue value; + private int maxListSize = 100; + private ToWidget toWidget; + + public DropdownWidget(String panelName, Class valueType) { + super(panelName); + this.valueType = valueType; + this.openOnHover = false; + } + + @Override + public void onInit() { + super.onInit(); + setValue(this.value.getValue(), false); + } + + @Override + public @NotNull List getChildren() { + return selected; + } + + protected IWidget valueToWidget(T v, boolean forSelectedDisplay) { + if (this.toWidget != null) { + return this.toWidget.apply(v, forSelectedDisplay); + } + return IKey.str(String.valueOf(v)).asWidget(); + } + + protected void setValue(T value, boolean updateValue) { + if (this.selected.hasValue()) { + this.selected.get().dispose(); + } + if (updateValue && this.value != null) this.value.setValue(value); + this.selected.set(valueToWidget(value, true)); + if (isValid()) { + this.selected.get().initialise(this, true); + scheduleResize(); + } + } + + @Override + protected Menu createMenu() { + return new Menu<>() + .widthRel(1f) + .coverChildrenHeight() + .child(new ListWidget<>() + .widthRel(1f) + .maxSize(this.maxListSize) + .children(this.values, v -> new ButtonWidget<>() + .widthRel(1f) + .coverChildrenHeight() + .child(valueToWidget(v, false)) + .onMousePressed(b -> { + setValue(v, true); + closeMenu(false); + return true; + }))); + } + + @Override + public boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return syncOrValue.isValueOfType(this.valueType); + } + + @Override + protected void setSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + super.setSyncOrValue(syncOrValue); + this.value = syncOrValue.castValueNullable(this.valueType); + } + + /** + * Deletes the current cached menu. This can be used if the list of options changes while the menu is open. + * If the menu is currently open it won't be affected. + */ + public void deleteMenu() { + setMenu(null); + } + + /** + * Sets a value handler that handles the selected value. + * + * @param value value handler + * @return this + */ + public W value(IValue value) { + setSyncOrValue(value); + return getThis(); + } + + /** + * Adds an option that can be selected. + * + * @param option selectable option + * @return this + */ + public W option(T option) { + this.values.add(option); + return getThis(); + } + + /** + * Adds an iterable of selectable options. + * + * @param options selectable options + * @return this + */ + public W options(Iterable options) { + for (T t : options) this.values.add(t); + return getThis(); + } + + /** + * Adds an array of selectable options. + * + * @param options selectable options + * @return this + */ + public W options(T... options) { + this.values.addAll(Arrays.asList(options)); + return getThis(); + } + + /** + * Clears all currently set selectable options. + * + * @return this + */ + public W clearOptions() { + this.values.clear(); + return getThis(); + } + + /** + * Sets a function which converts options into displayable widgets. The function is called once for each option when the menu is created + * and each time when a value is selected. The function itself has a value and boolean parameter. The value is the option which is + * displayed and the boolean is true if the widget is created for the selected display. There is no limit to what combination of widgets + * an option can be. Usually it is a text or a row with an icon and text. + * If this function is not a set {@link String#valueOf(Object)} is used to display text. + * + * @param toWidget function to create a display widget from an option + * @return this + * @see ToWidget + */ + public W optionToWidget(ToWidget toWidget) { + this.toWidget = toWidget; + return getThis(); + } + + /** + * Sets the maximum size of the list widget in pixel. By default, it is set to 100. + * + * @param maxListSize maximum list size in pixel + * @return this + */ + public W maxVerticalMenuSize(int maxListSize) { + this.maxListSize = maxListSize; + return getThis(); + } + + /** + * Sets the menu to open in the "up" direction. + * + * @return this + */ + public W directionUp() { + this.direction = Direction.UP; + return getThis(); + } + + /** + * Sets the menu to open in the "down" direction. + * + * @return this + */ + public W directionDown() { + this.direction = Direction.DOWN; + return getThis(); + } + + public interface ToWidget { + + /** + * A function to convert a value into a display widget. + * + * @param value value to display + * @param forSelectedDisplay if the widget is used on the button when the menu is closed + * @return this + */ + IWidget apply(T value, boolean forSelectedDisplay); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/menu/IMenuPart.java b/src/main/java/com/cleanroommc/modularui/widgets/menu/IMenuPart.java new file mode 100644 index 000000000..b5405ca19 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/menu/IMenuPart.java @@ -0,0 +1,13 @@ +package com.cleanroommc.modularui.widgets.menu; + +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.widget.WidgetTree; + +public interface IMenuPart extends IWidget { + + default boolean isSelfOrChildHovered() { + return isBelowMouse() || !WidgetTree.foreachChild(this, + w -> !(w instanceof IMenuPart menuPart ? menuPart.isSelfOrChildHovered() : w.isBelowMouse()), + false); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/menu/Menu.java b/src/main/java/com/cleanroommc/modularui/widgets/menu/Menu.java new file mode 100644 index 000000000..2b577e7f5 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/menu/Menu.java @@ -0,0 +1,45 @@ +package com.cleanroommc.modularui.widgets.menu; + +import com.cleanroommc.modularui.api.ITheme; +import com.cleanroommc.modularui.api.IThemeApi; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.theme.WidgetThemeEntry; +import com.cleanroommc.modularui.widget.ParentWidget; + +public class Menu> extends ParentWidget implements IMenuPart { + + private AbstractMenuButton menuSource; + + void setMenuSource(AbstractMenuButton source) { + this.menuSource = source; + } + + @Override + public void onMouseLeaveArea() { + super.onMouseLeaveArea(); + checkClose(); + } + + protected void checkClose() { + if (this.menuSource != null && !this.menuSource.isBelowMouse() && !isSelfOrChildHovered()) { + this.menuSource.closeMenu(true); + this.menuSource.checkClose(); + } + } + + @Override + protected void onChildAdd(IWidget child) { + super.onChildAdd(child); + if (!child.resizer().hasHeight()) { + child.resizer().height(12); + } + if (!child.resizer().hasWidth()) { + child.resizer().widthRel(1f); + } + } + + @Override + protected WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { + return theme.getWidgetTheme(IThemeApi.PANEL); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/menu/MenuPanel.java b/src/main/java/com/cleanroommc/modularui/widgets/menu/MenuPanel.java new file mode 100644 index 000000000..6ce837a83 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/menu/MenuPanel.java @@ -0,0 +1,37 @@ +package com.cleanroommc.modularui.widgets.menu; + +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.screen.ModularPanel; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public class MenuPanel extends ModularPanel { + + public MenuPanel(String name, IWidget menu) { + super(name); + fullScreenInvisible(); + child(menu); + themeOverride("modularui.context_menu"); + } + + public void openSubMenu(IWidget menuList) { + child(menuList); + } + + @Override + protected void onChildAdd(IWidget child) { + super.onChildAdd(child); + child.scheduleResize(); + } + + @Override + public boolean isDraggable() { + return false; + } + + @Override + public boolean closeOnOutOfBoundsClick() { + return true; + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java index 71d00e7cf..2f8056aad 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/FluidSlot.java @@ -20,7 +20,6 @@ import com.cleanroommc.modularui.value.sync.FluidSlotSyncHandler; import com.cleanroommc.modularui.widgets.AbstractFluidDisplayWidget; -import net.minecraft.client.renderer.GlStateManager; import net.minecraft.item.ItemStack; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.FluidTank; @@ -144,7 +143,7 @@ public WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { } public int getSlotHoverColor() { - WidgetThemeEntry theme = getWidgetTheme(getContext().getTheme(), SlotTheme.class); + WidgetThemeEntry theme = getWidgetTheme(getPanel().getTheme(), SlotTheme.class); return theme.getTheme().getSlotHoverColor(); } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java index 426ca58c4..2aed4373a 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java @@ -135,7 +135,7 @@ public WidgetThemeEntry getWidgetThemeInternal(ITheme theme) { } public int getSlotHoverColor() { - WidgetThemeEntry theme = getWidgetTheme(getContext().getTheme(), SlotTheme.class); + WidgetThemeEntry theme = getWidgetTheme(getPanel().getTheme(), SlotTheme.class); return theme.getTheme().getSlotHoverColor(); } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java index 5d217adca..576eb37ed 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java @@ -105,7 +105,7 @@ public void onUpdate() { @Override public void preDraw(ModularGuiContext context, boolean transformed) { if (transformed) { - WidgetThemeEntry entry = getWidgetTheme(context.getTheme(), TextFieldTheme.class); + WidgetThemeEntry entry = getWidgetTheme(getPanel().getTheme(), TextFieldTheme.class); TextFieldTheme widgetTheme = entry.getTheme(); this.renderer.setColor(this.textColor != null ? this.textColor : widgetTheme.getTextColor()); this.renderer.setCursorColor(this.textColor != null ? this.textColor : widgetTheme.getTextColor()); diff --git a/src/main/resources/assets/modularui/textures/gui/background/menu.png b/src/main/resources/assets/modularui/textures/gui/background/menu.png new file mode 100644 index 000000000..a30f24e36 Binary files /dev/null and b/src/main/resources/assets/modularui/textures/gui/background/menu.png differ diff --git a/src/main/resources/assets/modularui/themes.json b/src/main/resources/assets/modularui/themes.json index 33518a5a9..568314f6b 100644 --- a/src/main/resources/assets/modularui/themes.json +++ b/src/main/resources/assets/modularui/themes.json @@ -1,7 +1,8 @@ { "vanilla": "modularui:vanilla", "vanilla_dark": "modularui:vanilla_dark", + "modularui.context_menu": "modularui:context_menu", "screens": { "modularui:test": "vanilla_dark" } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/modularui/themes/context_menu.json b/src/main/resources/assets/modularui/themes/context_menu.json new file mode 100644 index 000000000..b72b97f45 --- /dev/null +++ b/src/main/resources/assets/modularui/themes/context_menu.json @@ -0,0 +1,27 @@ +{ + "parent": "DEFAULT", + "panel": { + "background": { + "type": "texture", + "id": "menu" + } + }, + "button": { + "background": "empty", + "textColor": "#404040", + "iconColor": "#404040", + "textShadow": false + }, + "toggleButton": { + "background": "empty", + "color": "#FFFFFF", + "textColor": "#404040", + "iconColor": "#404040", + "textShadow": false, + "selectedBackground": "empty", + "selectedColor": "#BBBBBB", + "selectedIconColor": "#404040", + "selectedTextColor": "#404040", + "selectedTextShadow": false + } +} diff --git a/src/test/java/com/cleanroommc/modularui/SizerTest.java b/src/test/java/com/cleanroommc/modularui/SizerTest.java index 8ce60f8c5..342692b8c 100644 --- a/src/test/java/com/cleanroommc/modularui/SizerTest.java +++ b/src/test/java/com/cleanroommc/modularui/SizerTest.java @@ -41,7 +41,7 @@ void assertArea(Area area, int x, int y, int w, int h) { } ModularScreen testPanel(ModularPanel panel) { - ModularScreen screen = new ModularScreen(panel); + ModularScreen screen = new ModularScreen(ModularUI.ID, panel); screen.getContext().setSettings(new UISettings()); ScreenWrapper wrapper = new ScreenWrapper(null, screen); screen.construct(wrapper);