diff --git a/src/main/java/cam72cam/mod/gui_v2/GuiUtils.java b/src/main/java/cam72cam/mod/gui_v2/GuiUtils.java new file mode 100644 index 000000000..209cef332 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/GuiUtils.java @@ -0,0 +1,84 @@ +package cam72cam.mod.gui_v2; + +import cam72cam.mod.MinecraftClient; +import cam72cam.mod.ModCore; +import cam72cam.mod.gui_v2.control.AbstractWidget; +import cam72cam.mod.gui_v2.control.PositionedPanel; +import cam72cam.mod.gui_v2.core.ScreenWrapper; +import cam72cam.mod.text.PlayerMessage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.event.ClickEvent; + +public class GuiUtils { + public static int mouseX; + public static int mouseY; + + public static int getMouseX() { + return mouseX; + } + + public static int getMouseY() { + return mouseY; + } + + public static int getTextWidth(PlayerMessage text) { + return getTextWidth(text.internal.getFormattedText()); + } + + public static int getTextWidth(String text) { + return Minecraft.getMinecraft().fontRenderer.getStringWidth(text); + } + + public static int getScreenWidth() { + return new ScaledResolution(Minecraft.getMinecraft()).getScaledWidth(); + } + + public static int getScreenHeight() { + return new ScaledResolution(Minecraft.getMinecraft()).getScaledHeight(); + } + + public static boolean isPrintable(char c) { + return !Character.isISOControl(c) && Character.isDefined(c); + } + + public static boolean insidePositionedPanel(AbstractWidget widget) { + AbstractWidget parent = widget; + while ((parent = parent.getParent()) != null) { + if (parent instanceof PositionedPanel) { + return true; + } + } + return false; + } + + /** Try to open an external link in player's browser */ + public void openLink(String url){ + if (ScreenWrapper.getInstance() != null) { + ITextComponent component = new TextComponentString(""); + component.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)); + ScreenWrapper.getInstance().handleComponentClick(component); + } else { + ModCore.error("Trying to open a link outside a screen: %s", url); + if (MinecraftClient.isReady() && MinecraftClient.getPlayer() != null) { + MinecraftClient.getPlayer().sendMessage(PlayerMessage.url(url)); + } + } + } + + /** Try to open an external link in player's browser */ + public static void openFile(String path){ + if (ScreenWrapper.getInstance() != null) { + ITextComponent component = new TextComponentString(""); + component.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE, path)); + ScreenWrapper.getInstance().handleComponentClick(component); + } else { + ModCore.error("Trying to open a file outside a screen: %s", path); + if (MinecraftClient.isReady() && MinecraftClient.getPlayer() != null) { + MinecraftClient.getPlayer().sendMessage(PlayerMessage.direct("Please check this location on your computer: " + path)); + } + } + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/AbstractPanel.java b/src/main/java/cam72cam/mod/gui_v2/control/AbstractPanel.java new file mode 100644 index 000000000..c168283a4 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/AbstractPanel.java @@ -0,0 +1,249 @@ +package cam72cam.mod.gui_v2.control; + +import cam72cam.mod.entity.Player; +import cam72cam.mod.gui_v2.GuiUtils; +import cam72cam.mod.gui_v2.core.actions.*; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.core.ScissorStack; +import cam72cam.mod.gui_v2.rendering.GuiRenderer; +import cam72cam.mod.input.Keyboard; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class AbstractPanel> extends AbstractWidget + implements IClickable, IDraggable, IUpdatable, IScrollable, IKeyboardListener { + private final List> children; + private final List> controller; + + private IFocusable active; + + public AbstractPanel(int width, int height) { + super(); + this.setBound(0, 0, width, height); + this.children = new ArrayList<>(); + this.controller = new ArrayList<>(); + +// this.setForegroundRenderFunc((gui, panel) -> panel.renderBound(gui, 0xFFFFFFFF)); + } + + public void addChildren(ILayoutable child) { + addChildren(Collections.singleton(child)); + } + + public void addChildren(ILayoutable... children) { + addChildren(Arrays.asList(children)); + } + + public void addChildren(Iterable> children) { + for (ILayoutable child : children) { + if (child == this) { + throw new IllegalArgumentException("Cannot add self as child panel!"); + } + this.children.add(child); + if (child instanceof AbstractWidget) { + ((AbstractWidget) child).parent = this; + } + } + layout(this.x(), this.y()); + } + + public List> getChildren() { + return children; + } + + public List> getVisibleChildren() { + return children.stream().filter(ILayoutable::isVisible).collect(Collectors.toList()); + } + + public void clearChildren() { + this.children.clear(); + } + + protected void addController(ILayoutable ctrl) { + this.controller.add(ctrl); + if (ctrl instanceof AbstractWidget) { + ((AbstractWidget) ctrl).parent = this; + } + } + + public void renderPanel(GuiRenderer renderer, ScissorStack stack) { + stack.pushPanel(this); + this.getVisibleChildren().forEach(child -> { + stack.push(child); + drawWidget(child, renderer, stack); + stack.pop(); + }); + stack.pop(); + stack.push(this); + this.controller.forEach(ctrl -> { + stack.push(ctrl); + drawWidget(ctrl, renderer, stack); + stack.pop(); + }); + drawWidget(this, renderer, stack); + stack.pop(); + } + + private void drawWidget(ILayoutable widget, GuiRenderer renderer, ScissorStack stack) { + if (widget instanceof IUpdatable) { + ((IUpdatable) widget).preRender(); + } + if (widget != this && widget instanceof AbstractPanel) { + ((AbstractPanel) widget).renderPanel(renderer, stack); + } else { + widget.renderBackground(renderer, stack); + widget.render(renderer, stack); + widget.renderForeground(renderer, stack); + } + if (widget instanceof IUpdatable) { + ((IUpdatable) widget).postRender(); + } + } + + public void renderBound(GuiRenderer renderer, int argb) { + renderer.drawRect(x(), y(), 1, height(), argb); + renderer.drawRect(x(), y(), width()-1, 1, argb); + renderer.drawRect(x() + width()-1, y(), 1, height(), argb); + renderer.drawRect(x(), y() + height()-1, width(), 1, argb); + } + + /* Indicate actual range excluded panel basics like ScrollPane's scroll bar */ + public int panelX() { + return x(); + } + public int panelY() { + return y(); + } + public int panelWidth() { + return width(); + } + public int panelHeight() { + return height(); + } + + protected boolean isHoveringPanel() { + return isHoveringPanel(GuiUtils.getMouseX(), GuiUtils.getMouseY()); + } + protected boolean isHoveringPanel(int mouseX, int mouseY) { + boolean flag = true; + if (parent != null) { + flag = parent.isHovering(mouseX, mouseY); + } + return flag && mouseX >= this.panelX() && mouseX <= this.panelX() + this.width() + && mouseY >= this.panelY() && mouseY <= this.panelY() + this.height(); + } + + @Override + public boolean onClick(Player.Hand hand, int x, int y) { + if (!isHovering()) { + return false; + } + if (castedStream(controller, IClickable.class).anyMatch(c -> c.onClick(hand, x, y))) { + return true; + } + return isHoveringPanel() && castedStream(getVisibleChildren(), IClickable.class) + .anyMatch(c -> c.onClick(hand, x, y)); + } + + @Override + public boolean onDrag(Player.Hand hand, int mouseX, int mouseY) { + if (active instanceof IDraggable) { + return ((IDraggable) active).onDrag(hand, mouseX, mouseY); + } + if (!isHovering()) { + return false; + } + if (castedStream(controller, IDraggable.class).anyMatch(c -> c.onDrag(hand, mouseX, mouseY))) { + return true; + } + return isHoveringPanel() && castedStream(getVisibleChildren(), IDraggable.class) + .anyMatch(c -> c.onDrag(hand, mouseX, mouseY)); + } + + @Override + public boolean onRelease(Player.Hand hand, int mouseX, int mouseY) { + if (active instanceof IDraggable) { + return ((IDraggable) active).onRelease(hand, mouseX, mouseY); + } + if (!isHovering()) { + return false; + } + if (castedStream(controller, IDraggable.class).anyMatch(c -> c.onRelease(hand, mouseX, mouseY))) { + return true; + } + return isHoveringPanel() && castedStream(getVisibleChildren(), IDraggable.class) + .anyMatch(c -> c.onRelease(hand, mouseX, mouseY)); + } + + @Override + public boolean onScroll(int mouseX, int mouseY, double deltaScroll) { + if (!isHovering()) { + return false; + } + if (castedStream(controller, IScrollable.class).anyMatch(c -> c.onScroll(mouseX, mouseY, deltaScroll))) { + return true; + } + return isHoveringPanel() && castedStream(getVisibleChildren(), IScrollable.class) + .anyMatch(c -> c.onScroll(mouseX, mouseY, deltaScroll)); + } + + @Override + public void onTick() { + castedStream(controller, IUpdatable.class).forEach(IUpdatable::onTick); + castedStream(getVisibleChildren(), IUpdatable.class).forEach(IUpdatable::onTick); + } + + @Override + public boolean onKeyPressed(Keyboard.KeyCode key) { + if (active instanceof IKeyboardListener) { + return ((IKeyboardListener) active).onKeyPressed(key); + } + if (castedStream(controller, IKeyboardListener.class).anyMatch(c -> c.onKeyPressed(key))) { + return true; + } + return castedStream(getVisibleChildren(), IKeyboardListener.class) + .anyMatch(c -> c.onKeyPressed(key)); + } + + @Override + public boolean onCharTyped(char ch) { + if (active instanceof IKeyboardListener) { + if (((IKeyboardListener) active).onCharTyped(ch)) return true; + } + if (castedStream(controller, IKeyboardListener.class).anyMatch(c -> c.onCharTyped(ch))) { + return true; + } + return castedStream(getVisibleChildren(), IKeyboardListener.class) + .anyMatch(c -> c.onCharTyped(ch)); + } + + @Override + protected void requestFocus(IFocusable focusing) { + if (this.parent != null) { + super.requestFocus(focusing); + return; + } + if (this.active != null) { + this.active.onFocusLost(); + } + focusing.onFocusGained(); + this.active = focusing; + } + @Override + public void freeFocus() { + if (this.parent != null) { + super.freeFocus(); + return; + } + if (this.active != null) { + this.active.onFocusLost(); + } + this.active = null; + } + + private static Stream castedStream(Collection elements, Class interface1) { + return elements.stream().filter(interface1::isInstance).map(interface1::cast); + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/AbstractWidget.java b/src/main/java/cam72cam/mod/gui_v2/control/AbstractWidget.java new file mode 100644 index 000000000..16c430b35 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/AbstractWidget.java @@ -0,0 +1,193 @@ +package cam72cam.mod.gui_v2.control; + +import cam72cam.mod.gui_v2.GuiUtils; +import cam72cam.mod.gui_v2.core.actions.IFocusable; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.core.ScissorStack; +import cam72cam.mod.gui_v2.rendering.GuiRenderFunc; +import cam72cam.mod.gui_v2.rendering.GuiRenderer; +import cam72cam.mod.text.PlayerMessage; +import cam72cam.mod.util.With; + +/** + * Basic UMC widget + */ +public abstract class AbstractWidget> + implements ILayoutable { + private PlayerMessage name; + private int nameColor; + + protected AbstractWidget parent; + + private boolean visible = true; + private boolean enabled = true; + + public AbstractWidget() {} + + /** + * Change current widget's visibility + */ + public void setVisible(boolean visible) { + this.visible = visible; + if (this instanceof IFocusable && ((IFocusable) this).isFocusing()) { + parent.freeFocus(); + } + if (GuiUtils.insidePositionedPanel(this)) { + requestLayout(); + } + } + + public boolean isVisible() { + return visible; + } + + /** + * Enable or disable current widget + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + /** + * Get/set current widget's display name + */ + public PlayerMessage getName() { + return name; + } + + public void setName(PlayerMessage text) { + this.name = text; + } + + /** Override the text color */ + public int getNameColor() { + return nameColor; + } + + public void setNameColor(int argb) { + this.nameColor = argb; + } + + /** + * Is mouse over? + */ + protected boolean isHovering() { + return isHovering(GuiUtils.getMouseX(), GuiUtils.getMouseY()); + } + + protected boolean isHovering(float mouseX, float mouseY) { + boolean flag = true; + if (parent != null) { + flag = parent.isHovering(mouseX, mouseY); + } + return flag && mouseX >= this.x() && mouseX <= this.x() + this.width() && mouseY >= this.y() && mouseY <= this.y() + this.height(); + } + + public AbstractWidget getParent() { + return parent; + } + + /* ILayoutable */ + private int x, y, width, height; + + @Override + public int x() { + return x; + } + @Override + public int y() { + return y; + } + @Override + public int width() { + return width; + } + @Override + public int height() { + return height; + } + @Override + public void setX(int x) { + this.x = x; + } + @Override + public void setY(int y) { + this.y = y; + } + @Override + public void setWidth(int width) { + this.width = width; + } + @Override + public void setHeight(int height) { + this.height = height; + } + @Override + public void setBound(int x, int y, int width, int height) { + this.setX(x); + this.setY(y); + this.setWidth(width); + this.setHeight(height); + } + + protected GuiRenderFunc background = (gui, widget) -> {}; + protected GuiRenderFunc content = (gui, widget) -> {}; + protected GuiRenderFunc foreground = (gui, widget) -> {}; + + @Override + public void renderBackground(GuiRenderer renderer, ScissorStack stack) { + try (With ctx = stack.applyScissor()) { + background.draw(renderer, (T) this); + } + } + @Override + public void render(GuiRenderer renderer, ScissorStack stack) { + try (With ctx = stack.applyScissor()) { + content.draw(renderer, (T) this); + } + } + @Override + public void renderForeground(GuiRenderer renderer, ScissorStack stack) { + try (With ctx = stack.applyScissor()) { + foreground.draw(renderer, (T) this); + } + } + + @Override + public void setBackgroundRenderFunc(GuiRenderFunc handler) { + background = handler; + } + @Override + public void setRenderFunc(GuiRenderFunc handler) { + content = handler; + } + @Override + public void setForegroundRenderFunc(GuiRenderFunc handler) { + foreground = handler; + } + + public void requestLayout() { + if (this.parent != null) { + parent.requestLayout(); + return; + } + //Only handle in root + layout(this.x(), this.y()); + } + + protected void requestFocus(IFocusable focusing) { + if (this.parent != null) { + this.parent.requestFocus(focusing); + } + //Implemented in AbstractPanel + } + protected void freeFocus() { + if (this.parent != null) { + this.parent.freeFocus(); + } + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/ComposedWidget.java b/src/main/java/cam72cam/mod/gui_v2/control/ComposedWidget.java new file mode 100644 index 000000000..427e3f2d8 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/ComposedWidget.java @@ -0,0 +1,112 @@ +package cam72cam.mod.gui_v2.control; + +import cam72cam.mod.entity.Player; +import cam72cam.mod.gui_v2.control.panel.SimplePane; +import cam72cam.mod.gui_v2.core.ScissorStack; +import cam72cam.mod.gui_v2.core.actions.*; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.rendering.GuiRenderer; +import cam72cam.mod.input.Keyboard; + +public abstract class ComposedWidget> extends AbstractWidget + implements IClickable, IDraggable, IUpdatable, IScrollable, IKeyboardListener { + + private final SimplePane internal; + + public ComposedWidget(int width, int height) { + this.internal = new SimplePane(width, height); + this.internal.parent = this; + this.setWidth(width); + this.setHeight(height); + } + + //Redirects + @Override + public void setX(int x) { + super.setX(x); + internal.setX(x); + } + @Override + public void setY(int y) { + super.setY(y); + internal.setY(y); + } + @Override + public void setWidth(int width) { + internal.setWidth(width); + super.setWidth(width); + } + @Override + public void setHeight(int height) { + internal.setHeight(height); + super.setHeight(height); + } + + @Override + public int width() { + return internal.width(); + } + @Override + public int height() { + return internal.height(); + } + + public int getChildRelativeX(ILayoutable child) { + return internal.getChildRelX(child); + } + public int getChildRelativeY(ILayoutable child) { + return internal.getChildRelY(child); + } + public void setChildRelativeX(ILayoutable child, int relX) { + internal.setChildPosition(child, relX, internal.getChildRelY(child)); + } + public void setChildRelativeY(ILayoutable child, int relY) { + internal.setChildPosition(child, internal.getChildRelX(child), relY); + } + + protected void addChildren(ILayoutable widget, int relX, int relY) { + internal.addChildren(widget, relX, relY); + } + + @Override + public void layout(int x, int y) { + this.setX(x); + this.setY(y); + internal.setBound(0, 0, width(), height()); + internal.layout(x, y); + } + @Override + public void render(GuiRenderer renderer, ScissorStack stack) { + stack.push(this); + internal.renderPanel(renderer, stack); + stack.pop(); + } + @Override + public boolean onClick(Player.Hand hand, int x, int y) { + return internal.onClick(hand, x, y); + } + @Override + public boolean onDrag(Player.Hand hand, int mouseX, int mouseY) { + return internal.onDrag(hand, mouseX, mouseY); + } + @Override + public boolean onRelease(Player.Hand hand, int mouseX, int mouseY) { + return internal.onRelease(hand, mouseX, mouseY); + } + @Override + public boolean onScroll(int mouseX, int mouseY, double deltaScroll) { + return internal.onScroll(mouseX, mouseY, deltaScroll); + } + @Override + public void onTick() { + internal.onTick(); + } + @Override + public boolean onKeyPressed(Keyboard.KeyCode key) { + return internal.onKeyPressed(key); + } + @Override + public boolean onCharTyped(char ch) { + return internal.onCharTyped(ch); + } +} \ No newline at end of file diff --git a/src/main/java/cam72cam/mod/gui_v2/control/PositionedPanel.java b/src/main/java/cam72cam/mod/gui_v2/control/PositionedPanel.java new file mode 100644 index 000000000..461a8fb53 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/PositionedPanel.java @@ -0,0 +1,41 @@ +package cam72cam.mod.gui_v2.control; + +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import it.unimi.dsi.fastutil.objects.Object2LongArrayMap; + +/** + * Abstraction of panels that could set children's relative positions statically, like AnchorPane + */ +public abstract class PositionedPanel> extends AbstractPanel { + private final Object2LongArrayMap> childrenPositions = new Object2LongArrayMap<>(); + + public PositionedPanel(int width, int height) { + super(width, height); + } + + public void addChildren(ILayoutable child, int relX, int relY) { + super.addChildren(child); + setChildPosition(child, relX, relY); + } + + public void setChildPosition(ILayoutable child, int relX, int relY) { + if (!getChildren().contains(child)) { + return; + } + childrenPositions.put(child, (long) relX << 32 | relY); + } + + public int getChildRelX(ILayoutable child) { + if (childrenPositions.containsKey(child)) { + return (int) (childrenPositions.getLong(child) >> 32); + } + return 0; + } + + public int getChildRelY(ILayoutable child) { + if (childrenPositions.containsKey(child)) { + return (int) childrenPositions.getLong(child); + } + return 0; + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/composed/ItemPicker.java b/src/main/java/cam72cam/mod/gui_v2/control/composed/ItemPicker.java new file mode 100644 index 000000000..bf7e83ea2 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/composed/ItemPicker.java @@ -0,0 +1,99 @@ +package cam72cam.mod.gui_v2.control.composed; + +import cam72cam.mod.gui_v2.control.ComposedWidget; +import cam72cam.mod.gui_v2.control.panel.HBox; +import cam72cam.mod.gui_v2.control.panel.VBox; +import cam72cam.mod.gui_v2.rendering.GuiRenderer; +import cam72cam.mod.item.ItemStack; + +import cam72cam.mod.gui_v2.control.panel.ScrollPane; +import cam72cam.mod.gui_v2.control.widget.Button; +import cam72cam.mod.gui_v2.control.widget.TextField; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class ItemPicker extends ComposedWidget { + private static final int SEARCH_HEIGHT = 20; + + private final TextField searchField; + private final ScrollPane scrollPane; + private final VBox rowsContainer; + + private final List allItems = new ArrayList<>(); + private final Consumer onItemSelected; + private List filteredItems = new ArrayList<>(); + + public ItemPicker(int width, int height, Consumer onItemSelected) { + super(width, height); + rowsContainer = new VBox(0); + scrollPane = new ScrollPane(width, height); + scrollPane.addChildren(rowsContainer); + addChildren(scrollPane, 0, 0); + + this.onItemSelected = onItemSelected; + + searchField = new TextField(width, SEARCH_HEIGHT, this::onSearchTextChanged); + searchField.setValidator(s -> true); + + addChildren(searchField, 0, 0); + addChildren(scrollPane, 0, SEARCH_HEIGHT); + } + + public void addItems(Collection items) { + allItems.addAll(items); + applyFilter(); + } + + public void setItems(List items) { + allItems.clear(); + allItems.addAll(items); + applyFilter(); + } + + public void clearItems() { + allItems.clear(); + applyFilter(); + } + + private void onSearchTextChanged(String text) { + applyFilter(); + } + + private void applyFilter() { + String searchText = searchField.getText().toLowerCase(); + String[] parts = searchText.isEmpty() ? new String[0] : searchText.split(" "); + filteredItems = allItems.stream() + .filter(stack -> Arrays.stream(parts).allMatch(part -> stack.getDisplayName().toLowerCase().contains(part))) + .collect(Collectors.toList()); + rebuildGrid(); + } + + private void rebuildGrid() { + rowsContainer.clearChildren(); + int cols = Math.max(1, (scrollPane.panelWidth()) / GuiRenderer.ITEM_SIZE); + HBox currentRow = null; + for (int i = 0; i < filteredItems.size(); i++) { + if (i % cols == 0) { + currentRow = new HBox(0); + rowsContainer.addChildren(currentRow); + } + int finalI = i; + currentRow.addChildren(Button.item(filteredItems.get(i), (btn) -> { + if (onItemSelected != null) { + onItemSelected.accept(filteredItems.get(finalI)); + } + })); + } + requestLayout(); + } + + @Override + public void layout(int x, int y) { + super.layout(x, y); + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/composed/NumberField.java b/src/main/java/cam72cam/mod/gui_v2/control/composed/NumberField.java new file mode 100644 index 000000000..0c810c5b1 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/composed/NumberField.java @@ -0,0 +1,122 @@ +package cam72cam.mod.gui_v2.control.composed; + +import cam72cam.mod.gui_v2.control.ComposedWidget; +import cam72cam.mod.gui_v2.control.widget.Button; +import cam72cam.mod.gui_v2.control.widget.Slider; +import cam72cam.mod.gui_v2.control.widget.TextField; +import cam72cam.mod.text.PlayerMessage; + +import java.util.function.Consumer; +import java.util.regex.Pattern; + +public class NumberField extends ComposedWidget { + private static final Pattern DECIMAL = Pattern.compile("-?\\d*\\.?\\d*"); + private static final Pattern INTEGER = Pattern.compile("-?\\d+"); + + private final TextField textField; + private final Slider slider; + private final Button button; + + private final Consumer callback; + private final boolean allowDecimal; + private final String formatter; + + private double value; + private boolean showSlider; + + public NumberField(int width, int height, PlayerMessage name, double min, double max, double orig, boolean allowDecimal, Consumer callback) { + super(width, height); + this.value = Math.max(min, Math.min(max, orig)); + this.button = Button.vanilla(height, height, PlayerMessage.direct("↺"), (hand, btn) -> onButtonChange()); + this.slider = Slider.horizontal(width - height, height, name, min, max, value, allowDecimal ? 4 : 0, this::onSliderChange); + this.textField = new TextField(width - height, height, this::onTextChange) { + @Override + public void onFocusLost() { + super.onFocusLost(); + String str = getText(); + double d = str.isEmpty() ? slider.getMinBound() : Double.parseDouble(str); + setText(String.format(formatter, Math.max(slider.getMinBound(), Math.min(slider.getMaxBound(), d)))); + } + }; + this.textField.setValidator(this::verify); + this.allowDecimal = allowDecimal; + this.formatter = allowDecimal ? "%.4f" : "%.0f"; + this.callback = callback; + + this.addChildren(this.textField, 0, 0); + this.addChildren(this.slider, 0, 0); + this.addChildren(this.button, width - height, 0); + //Wrapped true for triggering refreshment + this.showSlider = false; + this.onButtonChange(); + } + + //TODO Use HBox for auto scaling + @Override + public void setWidth(int width) { + super.setWidth(width); + if (this.textField != null) { + //Initialized, safe to use + int h = this.height(); + this.slider.setWidth(width - h); + this.textField.setWidth(width - h); + this.button.setWidth(h); + this.setChildRelativeX(this.button, this.slider.width()); + requestLayout(); + } + } + + @Override + public void setHeight(int height) { + super.setHeight(height); + if (this.textField != null) { + //Initialized, safe to use + int w = this.width(); + this.slider.setWidth(w - height); + this.textField.setWidth(w - height); + this.button.setWidth(height); + this.setChildRelativeX(this.button, this.slider.width()); + this.slider.setHeight(height); + this.textField.setHeight(height); + this.button.setHeight(height); + requestLayout(); + } + } + + protected void onButtonChange() { + this.showSlider = !this.showSlider; + if (this.showSlider) { + this.textField.setVisible(false); + this.slider.setValue(this.value); + this.slider.setVisible(true); + } else { + this.slider.setVisible(false); + this.textField.setText(String.format(formatter, this.value)); + this.textField.setVisible(true); + } + } + + protected void onSliderChange(Slider slider) { + this.value = slider.getValue(); + if (!allowDecimal) { + this.value = Math.round(this.value); + } + callback.accept(this.value); + } + + protected void onTextChange(String newText) { + if (newText.isEmpty()) { + this.value = 0; + return; + } + this.value = Double.parseDouble(newText); + callback.accept(this.value); + } + + protected boolean verify(String text) { + if (text.isEmpty()) { + return true; + } + return allowDecimal ? DECIMAL.matcher(text).matches() : INTEGER.matcher(text).matches(); + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/panel/AnchorPane.java b/src/main/java/cam72cam/mod/gui_v2/control/panel/AnchorPane.java new file mode 100644 index 000000000..ac1a12548 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/panel/AnchorPane.java @@ -0,0 +1,99 @@ +package cam72cam.mod.gui_v2.control.panel; + +import cam72cam.mod.gui_v2.GuiUtils; +import cam72cam.mod.gui_v2.control.PositionedPanel; +import cam72cam.mod.gui_v2.core.layout.HorizontalAlign; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.core.layout.VerticalAlign; + +import java.util.HashMap; +import java.util.Map; + +public class AnchorPane extends PositionedPanel { + private final Map, AnchorInfo> anchorMap = new HashMap<>(); + + public AnchorPane(int width, int height) { + super(width, height); + } + + public static AnchorPane fullScreen() { + return new AnchorPane(GuiUtils.getScreenWidth(), GuiUtils.getScreenHeight()); + } + + @Override + public void addChildren(ILayoutable child, int relX, int relY) { + addChildren(child, HorizontalAlign.LEFT, relX, VerticalAlign.TOP, relY); + } + + @Override + public void addChildren(Iterable> children) { + super.addChildren(children); + for (ILayoutable child : children) { + anchorMap.put(child, new AnchorInfo(HorizontalAlign.LEFT, 0, VerticalAlign.TOP, 0)); + } + } + + public void addChildren(ILayoutable child, HorizontalAlign hAlign, int marginX, VerticalAlign vAlign, int marginY) { + super.addChildren(child); + anchorMap.put(child, new AnchorInfo(hAlign, marginX, vAlign, marginY)); + requestLayout(); + } + + public void setChildAnchor(ILayoutable child, HorizontalAlign hAlign, int marginX, VerticalAlign vAlign, int marginY) { + if (!anchorMap.containsKey(child)) + throw new IllegalArgumentException("AnchorPane does not contain child"); + anchorMap.put(child, new AnchorInfo(hAlign, marginX, vAlign, marginY)); + requestLayout(); + } + + @Override + public void layout(int x, int y) { + this.setX(x); + this.setY(y); + + int panelW = width(); + int panelH = height(); + + for (ILayoutable child : getVisibleChildren()) { + AnchorInfo info = anchorMap.get(child); + if (info != null) { + int relX = 0, relY = 0; + + switch (info.hAlign) { + case LEFT: relX = info.marginX; break; + case MIDDLE: relX = (panelW - child.width()) / 2; break; + case RIGHT: relX = panelW - child.width() - info.marginX; break; + } + + switch (info.vAlign) { + case TOP: relY = info.marginY; break; + case MIDDLE: relY = (panelH - child.height()) / 2; break; + case BOTTOM: relY = panelH - child.height() - info.marginY; break; + } + + setChildPosition(child, relX, relY); + } + + int childX = x + getChildRelX(child); + int childY = y + getChildRelY(child); + child.setX(childX); + child.setY(childY); + child.layout(childX, childY); + } + } + + private static class AnchorInfo { + final HorizontalAlign hAlign; + final int marginX; + + final VerticalAlign vAlign; + final int marginY; + + AnchorInfo(HorizontalAlign hAlign, int marginX, VerticalAlign vAlign, int marginY) { + this.hAlign = hAlign; + this.marginX = marginX; + this.vAlign = vAlign; + this.marginY = marginY; + } + } +} \ No newline at end of file diff --git a/src/main/java/cam72cam/mod/gui_v2/control/panel/HBox.java b/src/main/java/cam72cam/mod/gui_v2/control/panel/HBox.java new file mode 100644 index 000000000..78cd03eed --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/panel/HBox.java @@ -0,0 +1,96 @@ +package cam72cam.mod.gui_v2.control.panel; + +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.core.layout.VerticalAlign; +import cam72cam.mod.gui_v2.control.AbstractPanel; + +public class HBox extends AbstractPanel { + private int spacing; + private VerticalAlign alignType; + + public HBox(int spacing) { + this(spacing, VerticalAlign.TOP); + } + + public HBox(int spacing, VerticalAlign alignType) { + super(0, 0); + this.spacing = spacing; + this.alignType = alignType; + } + + public int getSpacing() { + return spacing; + } + + public void setSpacing(int spacing) { + this.spacing = spacing; + requestLayout(); + } + + public VerticalAlign getAlignType() { + return alignType; + } + + public void setAlignType(VerticalAlign alignType) { + this.alignType = alignType; + } + + @Override + public void layout(int x, int y) { + this.setX(x); + this.setY(y); + + int maxHeight = getVisibleChildren().stream() + .mapToInt(ILayoutable::height) + .max().orElse(0); + + int currentX = x; + int totalWidth = 0; + + for (ILayoutable widget : getVisibleChildren()) { + int childWidth = widget.width(); + int childHeight = widget.height(); + + int childYOffset; + switch (alignType) { + case MIDDLE: + childYOffset = (maxHeight - childHeight) / 2; + break; + case BOTTOM: + childYOffset = maxHeight - childHeight; + break; + case TOP: + default: + childYOffset = 0; + break; + } + + widget.setX(currentX); + widget.setY(childYOffset); + widget.layout(currentX, y + childYOffset); + + currentX += childWidth + spacing; + totalWidth += childWidth; + } + + if (getVisibleChildren().size() > 1) { + totalWidth += spacing * (getVisibleChildren().size() - 1); + } + setWHInternal(totalWidth, maxHeight); + } + + @Override + public void setWidth(int width) { + // NO-OP for HBox + } + + @Override + public void setHeight(int height) { + // NO-OP for HBox + } + + protected void setWHInternal(int width, int height) { + super.setWidth(width); + super.setHeight(height); + } +} \ No newline at end of file diff --git a/src/main/java/cam72cam/mod/gui_v2/control/panel/ScrollPane.java b/src/main/java/cam72cam/mod/gui_v2/control/panel/ScrollPane.java new file mode 100644 index 000000000..8f32241e5 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/panel/ScrollPane.java @@ -0,0 +1,87 @@ +package cam72cam.mod.gui_v2.control.panel; + +import cam72cam.mod.gui_v2.control.AbstractPanel; +import cam72cam.mod.gui_v2.control.widget.Slider; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.core.actions.IScrollable; +import cam72cam.mod.text.PlayerMessage; + +//TODO Controller position and visibility +public class ScrollPane extends AbstractPanel implements IScrollable { + private final Slider controller; + // 0 stands for top + private double scrolled; + private int contentHeight; + + public ScrollPane(int width, int height) { + super(width, height); + this.controller = Slider.vertical(10, height, PlayerMessage.direct(""), + 0, 1, 0, 0, this::onControllerChange); + addController(controller); + } + + public static ScrollPane vertical(int width, int height) { + return new ScrollPane(width, height); + } + + @Override + public void layout(int x, int y) { + this.setX(x); + this.setY(y); + + contentHeight = getVisibleChildren().stream().mapToInt(ILayoutable::height).sum(); + + double maxScroll = contentHeight - height(); + if (maxScroll <= 0) { + maxScroll = 0; + scrolled = 0; + controller.setHandleSize(controller.height()); + } else { + int handleSize = controller.height(); + handleSize *= height(); + handleSize /= contentHeight; + controller.setHandleSize(handleSize); + } + + double scrollOffset = scrolled * maxScroll; + + int currentY = y() - (int) scrollOffset; + for (ILayoutable widget : getVisibleChildren()) { + widget.setX(x()); + widget.setY(currentY); + widget.layout(x(), currentY); + currentY += widget.height(); + } + + this.controller.layout(x() + width() - controller.width(), y()); + } + + @Override + public int panelWidth() { + return super.width() - controller.width(); + } + + @Override + public boolean onScroll(int mouseX, int mouseY, double deltaScroll) { + if (!isHovering()) return false; + + double maxScroll = Math.max(0, contentHeight - height()); + if (maxScroll <= 0) return false; + + //Fixed at 20px + double step = 20.0 / maxScroll; + if (deltaScroll > 0) { + scrolled = Math.max(0, scrolled - step); + } else if (deltaScroll < 0) { + scrolled = Math.min(1.0, scrolled + step); + } + + this.controller.setValue(scrolled); + return true; + } + + private void onControllerChange(Slider ctrl) { + scrolled = ctrl.getValue(); + requestLayout(); + } +} \ No newline at end of file diff --git a/src/main/java/cam72cam/mod/gui_v2/control/panel/SimplePane.java b/src/main/java/cam72cam/mod/gui_v2/control/panel/SimplePane.java new file mode 100644 index 000000000..01230fe1e --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/panel/SimplePane.java @@ -0,0 +1,34 @@ +package cam72cam.mod.gui_v2.control.panel; + +import cam72cam.mod.gui_v2.GuiUtils; +import cam72cam.mod.gui_v2.control.PositionedPanel; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; + +/** + * Fixed size panel + */ +public class SimplePane extends PositionedPanel { + public SimplePane(int width, int height) { + super(width, height); + } + + public static SimplePane fullScreen() { + return new SimplePane(GuiUtils.getScreenWidth(), GuiUtils.getScreenHeight()); + } + + @Override + public void layout(int x, int y) { + this.setX(x); + this.setY(y); + int width = 0, height = 0; + for (ILayoutable child : getVisibleChildren()) { + int childX = x + getChildRelX(child); + int childY = y + getChildRelY(child); + child.layout(childX, childY); + width = Math.max(child.width() + childX - x, width); + height = Math.max(child.height() + childY - y, height); + } + this.setWidth(width); + this.setHeight(height); + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/panel/VBox.java b/src/main/java/cam72cam/mod/gui_v2/control/panel/VBox.java new file mode 100644 index 000000000..c8a847941 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/panel/VBox.java @@ -0,0 +1,83 @@ +package cam72cam.mod.gui_v2.control.panel; + +import cam72cam.mod.gui_v2.core.layout.HorizontalAlign; +import cam72cam.mod.gui_v2.core.layout.ILayoutable; +import cam72cam.mod.gui_v2.control.AbstractPanel; + +public class VBox extends AbstractPanel { + private int spacing; + private HorizontalAlign alignType; + + public VBox(int spacing) { + this(spacing, HorizontalAlign.LEFT); + } + + public VBox(int spacing, HorizontalAlign alignType) { + //Left empty for auto width/height + super(0, 0); + this.spacing = spacing; + this.alignType = alignType; + } + + public int getSpacing() { + return spacing; + } + + public void setSpacing(int spacing) { + this.spacing = spacing; + requestLayout(); + } + + public HorizontalAlign getAlignType() { + return alignType; + } + + public void setAlignType(HorizontalAlign alignType) { + this.alignType = alignType; + } + + @Override + public void layout(int x, int y) { + this.setX(x); + this.setY(y); + int maxWidth = getVisibleChildren().stream().mapToInt(ILayoutable::width).max().orElse(0); + int currentHeight = y; + for (ILayoutable widget : getVisibleChildren()) { + int childWidth = widget.width(); + int childXOffset; + switch (alignType) { + case MIDDLE: + childXOffset = (maxWidth - childWidth) / 2; + break; + case RIGHT: + childXOffset = maxWidth - childWidth; + break; + case LEFT: + default: + childXOffset = 0; + break; + } + widget.setX(childXOffset); + widget.setY(currentHeight); + widget.layout(x + childXOffset, currentHeight); + currentHeight += widget.height() + spacing; + } + int totalHeight = currentHeight - y - spacing; + setWHInternal(maxWidth, Math.max(totalHeight, 0)); + } + + @Override + public void setWidth(int width) { + //NO-OP for VBox + } + + @Override + public void setHeight(int height) { + //NO-OP for VBox + } + + protected void setWHInternal(int width, int height) { + super.setWidth(width); + super.setHeight(height); + } +} diff --git a/src/main/java/cam72cam/mod/gui_v2/control/widget/Button.java b/src/main/java/cam72cam/mod/gui_v2/control/widget/Button.java new file mode 100644 index 000000000..1652f18c9 --- /dev/null +++ b/src/main/java/cam72cam/mod/gui_v2/control/widget/Button.java @@ -0,0 +1,120 @@ +package cam72cam.mod.gui_v2.control.widget; + +import cam72cam.mod.entity.Player; +import cam72cam.mod.gui_v2.control.AbstractWidget; +import cam72cam.mod.gui_v2.core.actions.IClickable; +import cam72cam.mod.gui_v2.core.actions.ITooltipper; +import cam72cam.mod.gui_v2.rendering.GuiRenderer; +import cam72cam.mod.item.ItemStack; +import cam72cam.mod.render.opengl.RenderContext; +import cam72cam.mod.render.opengl.RenderState; +import cam72cam.mod.render.opengl.Texture; +import cam72cam.mod.resource.Identifier; +import cam72cam.mod.text.PlayerMessage; +import cam72cam.mod.util.With; + +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class Button extends AbstractWidget