diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java b/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java index 78aa79b..b37a6a8 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/Colors.java @@ -34,7 +34,13 @@ public class Colors { DEBUG_BLUE = toVector(0xFF0000FF), DEBUG_CYAN = toVector(0xFF00FFFF), DEBUG_MAGENTA = toVector(0xFFFF00FF), - DEBUG_YELLOW = toVector(0xFFFFFF00); + DEBUG_YELLOW = toVector(0xFFFFFF00), + + LIGHT_GRAY = toVector(0xFFCBCBD0), + BLUE = toVector(0xFF37A2E6), + HOVER_BLUE = toVector(0xFFC3E4F7), + DISABLED_GRAY = toVector(0xFFE5E5E5), + DISABLED_BLUE = toVector(0xFFB2D8ED); public static Vec4 toVector(int argb) { return toVector(argb, new Vec4()); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java b/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java index 1fea54e..658b02d 100755 --- a/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/flat/RenderTarget.java @@ -189,13 +189,10 @@ public class RenderTarget { public void addCustomRenderer(Renderable renderable) { assembleCurrentClipFromFaces(); - assembled.add( - new Clip( - maskStack, - getTransform(), - renderable - ) - ); + + float depth = this.depth--; + Mat4 transform = new Mat4().translate(0, 0, depth).mul(getTransform()); + assembled.add(new Clip(maskStack, transform, renderable)); } protected void addFaceToCurrentClip(Face face) { diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java new file mode 100644 index 0000000..cd30152 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java @@ -0,0 +1,151 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.client.graphics.gui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.function.Consumer; + +import org.lwjgl.glfw.GLFW; + +import com.google.common.eventbus.Subscribe; + +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.gui.event.ButtonEvent; +import ru.windcorp.progressia.client.graphics.gui.event.EnableEvent; +import ru.windcorp.progressia.client.graphics.gui.event.FocusEvent; +import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; + +public abstract class BasicButton extends Component { + + private final Label label; + + private boolean isPressed = false; + private final Collection> actions = Collections.synchronizedCollection(new ArrayList<>()); + + public BasicButton(String name, String label, Font labelFont) { + super(name); + this.label = new Label(name + ".Label", labelFont, label); + + setLayout(new LayoutAlign(10)); + addChild(this.label); + + setFocusable(true); + reassembleAt(ARTrigger.HOVER, ARTrigger.FOCUS, ARTrigger.ENABLE); + + // Click triggers + addListener(KeyEvent.class, e -> { + if (e.isRepeat()) { + return false; + } else if ( + e.isLeftMouseButton() || + e.getKey() == GLFW.GLFW_KEY_SPACE || + e.getKey() == GLFW.GLFW_KEY_ENTER + ) { + setPressed(e.isPress()); + return true; + } else { + return false; + } + }); + + addListener(new Object() { + + // Release when losing focus + @Subscribe + public void onFocusChange(FocusEvent e) { + if (!e.getNewState()) { + setPressed(false); + } + } + + // Release when hover ends + @Subscribe + public void onHoverEnded(HoverEvent e) { + if (!e.isNowHovered()) { + setPressed(false); + } + } + + // Release when disabled + @Subscribe + public void onDisabled(EnableEvent e) { + if (!e.getComponent().isEnabled()) { + setPressed(false); + } + } + + // Trigger virtualClick when button is released + @Subscribe + public void onRelease(ButtonEvent.Release e) { + virtualClick(); + } + + }); + } + + public BasicButton(String name, String label) { + this(name, label, new Font()); + } + + public boolean isPressed() { + return isPressed; + } + + public void click() { + setPressed(true); + setPressed(false); + } + + public void setPressed(boolean isPressed) { + if (this.isPressed != isPressed) { + this.isPressed = isPressed; + + if (isPressed) { + takeFocus(); + } + + dispatchEvent(ButtonEvent.create(this, this.isPressed)); + } + } + + public BasicButton addAction(Consumer action) { + this.actions.add(Objects.requireNonNull(action, "action")); + return this; + } + + public boolean removeAction(Consumer action) { + return this.actions.remove(action); + } + + public void virtualClick() { + this.actions.forEach(action -> { + action.accept(this); + }); + } + + public Label getLabel() { + return label; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Button.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Button.java new file mode 100644 index 0000000..bbeb361 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Button.java @@ -0,0 +1,79 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.client.graphics.gui; + +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.Colors; + +public class Button extends BasicButton { + + public Button(String name, String label, Font labelFont) { + super(name, label, labelFont); + } + + public Button(String name, String label) { + this(name, label, new Font()); + } + + @Override + protected void assembleSelf(RenderTarget target) { + // Border + + Vec4 borderColor; + if (isPressed() || isHovered() || isFocused()) { + borderColor = Colors.BLUE; + } else { + borderColor = Colors.LIGHT_GRAY; + } + target.fill(getX(), getY(), getWidth(), getHeight(), borderColor); + + // Inside area + + if (isPressed()) { + // Do nothing + } else { + Vec4 backgroundColor; + if (isHovered() && isEnabled()) { + backgroundColor = Colors.HOVER_BLUE; + } else { + backgroundColor = Colors.WHITE; + } + target.fill(getX() + 2, getY() + 2, getWidth() - 4, getHeight() - 4, backgroundColor); + } + + // Change label font color + + if (isPressed()) { + getLabel().setFont(getLabel().getFont().withColor(Colors.WHITE)); + } else { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLACK)); + } + } + + @Override + protected void postAssembleSelf(RenderTarget target) { + // Apply disable tint + + if (!isEnabled()) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.toVector(0x88FFFFFF)); + } + } +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Checkbox.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Checkbox.java new file mode 100644 index 0000000..cb2d52a --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Checkbox.java @@ -0,0 +1,149 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.client.graphics.gui; + +import glm.vec._2.i.Vec2i; +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.font.Typefaces; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutHorizontal; + +public class Checkbox extends BasicButton { + + private class Tick extends Component { + + public Tick() { + super(Checkbox.this.getName() + ".Tick"); + + setPreferredSize(new Vec2i(Typefaces.getDefault().getLineHeight() * 3 / 2)); + } + + @Override + protected void assembleSelf(RenderTarget target) { + + int size = getPreferredSize().x; + int x = getX(); + int y = getY() + (getHeight() - size) / 2; + + // Border + + Vec4 borderColor; + if (Checkbox.this.isPressed() || Checkbox.this.isHovered() || Checkbox.this.isFocused()) { + borderColor = Colors.BLUE; + } else { + borderColor = Colors.LIGHT_GRAY; + } + target.fill(x, y, size, size, borderColor); + + // Inside area + + if (Checkbox.this.isPressed()) { + // Do nothing + } else { + Vec4 backgroundColor; + if (Checkbox.this.isHovered() && Checkbox.this.isEnabled()) { + backgroundColor = Colors.HOVER_BLUE; + } else { + backgroundColor = Colors.WHITE; + } + target.fill(x + 2, y + 2, size - 4, size - 4, backgroundColor); + } + + // "Tick" + + if (Checkbox.this.isChecked()) { + target.fill(x + 4, y + 4, size - 8, size - 8, Colors.BLUE); + } + } + + } + + private boolean checked; + + public Checkbox(String name, String label, Font labelFont, boolean check) { + super(name, label, labelFont); + this.checked = check; + + assert getChildren().size() == 1 : "Checkbox expects that BasicButton contains exactly one child"; + Component basicChild = getChild(0); + + Panel panel = new Panel(getName() + ".LabelAndTick", new LayoutHorizontal(0, 10)); + removeChild(basicChild); + setLayout(new LayoutAlign(0, 0.5f, 10)); + panel.setLayoutHint(basicChild.getLayoutHint()); + panel.addChild(new Tick()); + panel.addChild(basicChild); + addChild(panel); + + addAction(b -> switchState()); + } + + public Checkbox(String name, String label, Font labelFont) { + this(name, label, labelFont, false); + } + + public Checkbox(String name, String label, boolean check) { + this(name, label, new Font(), check); + } + + public Checkbox(String name, String label) { + this(name, label, false); + } + + public void switchState() { + setChecked(!isChecked()); + } + + /** + * @return the checked + */ + public boolean isChecked() { + return checked; + } + + /** + * @param checked the checked to set + */ + public void setChecked(boolean checked) { + this.checked = checked; + } + + @Override + protected void assembleSelf(RenderTarget target) { + // Change label font color + + if (isPressed()) { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLUE)); + } else { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLACK)); + } + } + + @Override + protected void postAssembleSelf(RenderTarget target) { + // Apply disable tint + + if (!isEnabled()) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.toVector(0x88FFFFFF)); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java index 4a131d8..bb6f24d 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Component.java @@ -19,18 +19,23 @@ package ru.windcorp.progressia.client.graphics.gui; import java.util.Collections; +import java.util.EnumMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import org.lwjgl.glfw.GLFW; import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import glm.vec._2.i.Vec2i; import ru.windcorp.progressia.client.graphics.backend.InputTracker; import ru.windcorp.progressia.client.graphics.flat.RenderTarget; import ru.windcorp.progressia.client.graphics.gui.event.ChildAddedEvent; import ru.windcorp.progressia.client.graphics.gui.event.ChildRemovedEvent; +import ru.windcorp.progressia.client.graphics.gui.event.EnableEvent; import ru.windcorp.progressia.client.graphics.gui.event.FocusEvent; import ru.windcorp.progressia.client.graphics.gui.event.HoverEvent; import ru.windcorp.progressia.client.graphics.gui.event.ParentChangedEvent; @@ -61,6 +66,8 @@ public class Component extends Named { private Object layoutHint = null; private Layout layout = null; + + private boolean isEnabled = true; private boolean isFocusable = false; private boolean isFocused = false; @@ -285,9 +292,30 @@ public class Component extends Named { return this; } + /** + * Checks whether this component is focusable. A component needs to be + * focusable to become focused. A component that is focusable may not + * necessarily be ready to gain focus (see {@link #canGainFocusNow()}). + * + * @return {@code true} iff the component is focusable + * @see #canGainFocusNow() + */ public boolean isFocusable() { return isFocusable; } + + /** + * Checks whether this component can become focused at this moment. + *

+ * The implementation of this method in {@link Component} considers the + * component a focus candidate if it is both focusable and enabled. + * + * @return {@code true} iff the component can receive focus + * @see #isFocusable() + */ + public boolean canGainFocusNow() { + return isFocusable() && isEnabled(); + } public Component setFocusable(boolean focusable) { this.isFocusable = focusable; @@ -337,7 +365,7 @@ public class Component extends Named { return; } - if (component.isFocusable()) { + if (component.canGainFocusNow()) { setFocused(false); component.setFocused(true); return; @@ -379,7 +407,7 @@ public class Component extends Named { return; } - if (component.isFocusable()) { + if (component.canGainFocusNow()) { setFocused(false); component.setFocused(true); return; @@ -432,13 +460,52 @@ public class Component extends Named { return null; } + + public boolean isEnabled() { + return isEnabled; + } + + /** + * Enables or disables this component. An {@link EnableEvent} is dispatched + * if the state changes. + * + * @param enabled {@code true} to enable the component, {@code false} to + * disable the component + * @see #setEnabledRecursively(boolean) + */ + public void setEnabled(boolean enabled) { + if (this.isEnabled != enabled) { + if (isFocused() && isEnabled()) { + focusNext(); + } + + if (isEnabled()) { + setHovered(false); + } + + this.isEnabled = enabled; + dispatchEvent(new EnableEvent(this)); + } + } + + /** + * Enables or disables this component and all of its children recursively. + * + * @param enabled {@code true} to enable the components, {@code false} to + * disable the components + * @see #setEnabled(boolean) + */ + public void setEnabledRecursively(boolean enabled) { + setEnabled(enabled); + getChildren().forEach(c -> c.setEnabledRecursively(enabled)); + } public boolean isHovered() { return isHovered; } protected void setHovered(boolean isHovered) { - if (this.isHovered != isHovered) { + if (this.isHovered != isHovered && isEnabled()) { this.isHovered = isHovered; if (!isHovered && !getChildren().isEmpty()) { @@ -502,7 +569,7 @@ public class Component extends Named { } protected void handleInput(Input input) { - if (inputBus != null) { + if (inputBus != null && isEnabled()) { inputBus.dispatch(input); } } @@ -598,6 +665,17 @@ public class Component extends Named { } } + /** + * Schedules the reassembly to occur. + *

+ * This method is invoked in root components whenever a + * {@linkplain #requestReassembly() reassembly request} is made by one of + * its children. When creating the dedicated root component, override this + * method to perform any implementation-specific actions that will cause a + * reassembly as soon as possible. + *

+ * The default implementation of this method does nothing. + */ protected void handleReassemblyRequest() { // To be overridden } @@ -637,6 +715,135 @@ public class Component extends Named { protected void assembleChildren(RenderTarget target) { getChildren().forEach(child -> child.assemble(target)); } + + /* + * Automatic Reassembly + */ + + /** + * The various kinds of changes that may be used with + * {@link Component#reassembleAt(ARTrigger...)}. + */ + protected static enum ARTrigger { + /** + * Reassemble the component whenever its hover status changes, e.g. + * whenever the pointer enters or leaves its bounds. + */ + HOVER, + + /** + * Reassemble the component whenever it gains or loses focus. + *

+ * Component must be focusable to be able to gain focus. The + * component will not be reassembled unless + * {@link Component#setFocusable(boolean) setFocusable(true)} has been + * invoked. + */ + FOCUS, + + /** + * Reassemble the component whenever it is enabled or disabled. + */ + ENABLE + } + + /** + * All trigger objects (event listeners) that are currently registered with + * {@link #eventBus}. The field is {@code null} until the first trigger is + * installed. + */ + private Map autoReassemblyTriggerObjects = null; + + private Object createTriggerObject(ARTrigger type) { + switch (type) { + case HOVER: + return new Object() { + @Subscribe + public void onHoverChanged(HoverEvent e) { + requestReassembly(); + } + }; + case FOCUS: + return new Object() { + @Subscribe + public void onFocusChanged(FocusEvent e) { + requestReassembly(); + } + }; + case ENABLE: + return new Object() { + @Subscribe + public void onEnabled(EnableEvent e) { + requestReassembly(); + } + }; + default: + throw new NullPointerException("type"); + } + } + + /** + * Requests that {@link #requestReassembly()} is invoked on this component + * whenever any of the specified changes occur. Duplicate attempts to + * register the same trigger are silently ignored. + *

+ * {@code triggers} may be empty, which results in a no-op. It must not be + * {@code null}. + * + * @param triggers the {@linkplain ARTrigger triggers} to + * request reassembly with. + * @see #disableAutoReassemblyAt(ARTrigger...) + */ + protected synchronized void reassembleAt(ARTrigger... triggers) { + + Objects.requireNonNull(triggers, "triggers"); + if (triggers.length == 0) + return; + + if (autoReassemblyTriggerObjects == null) { + autoReassemblyTriggerObjects = new EnumMap<>(ARTrigger.class); + } + + for (ARTrigger trigger : triggers) { + if (!autoReassemblyTriggerObjects.containsKey(trigger)) { + Object triggerObject = createTriggerObject(trigger); + addListener(trigger); + autoReassemblyTriggerObjects.put(trigger, triggerObject); + } + } + + } + + /** + * Requests that {@link #requestReassembly()} is no longer invoked on this + * component whenever any of the specified changes occur. After a trigger is + * removed, it may be reinstalled with + * {@link #reassembleAt(ARTrigger...)}. Attempts to remove a + * nonexistant trigger are silently ignored. + *

+ * {@code triggers} may be empty, which results in a no-op. It must not be + * {@code null}. + * + * @param triggers the {@linkplain ARTrigger triggers} to remove + * @see #reassemblyAt(ARTrigger...) + */ + protected synchronized void disableAutoReassemblyAt(ARTrigger... triggers) { + + Objects.requireNonNull(triggers, "triggers"); + if (triggers.length == 0) + return; + + if (autoReassemblyTriggerObjects == null) + return; + + for (ARTrigger trigger : triggers) { + Object triggerObject = autoReassemblyTriggerObjects.remove(trigger); + if (triggerObject != null) { + removeListener(trigger); + } + } + + } // /** // * Returns a component that displays this component in its center. diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java index f7dbe33..4450f33 100755 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/Label.java @@ -82,6 +82,11 @@ public class Label extends Component { public Font getFont() { return font; } + + public void setFont(Font font) { + this.font = font; + requestReassembly(); + } public String getCurrentText() { return currentText; @@ -96,11 +101,7 @@ public class Label extends Component { float startX = getX() + font.getAlign() * (getWidth() - currentSize.x); target.pushTransform( - new Mat4().identity().translate(startX, getY(), -1000) // TODO wtf - // is this - // magic - // <--- - .scale(2) + new Mat4().identity().translate(startX, getY(), 0).scale(2) ); target.addCustomRenderer(font.assemble(currentText, maxWidth)); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java new file mode 100644 index 0000000..bb9cee6 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java @@ -0,0 +1,205 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.client.graphics.gui; + +import org.lwjgl.glfw.GLFW; + +import glm.vec._2.i.Vec2i; +import glm.vec._4.Vec4; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.font.Typefaces; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutHorizontal; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; + +public class RadioButton extends BasicButton { + + private class Tick extends Component { + + public Tick() { + super(RadioButton.this.getName() + ".Tick"); + + setPreferredSize(new Vec2i(Typefaces.getDefault().getLineHeight() * 3 / 2)); + } + + private void cross(RenderTarget target, int x, int y, int size, Vec4 color) { + target.fill(x + 4, y, size - 8, size, color); + target.fill(x + 2, y + 2, size - 4, size - 4, color); + target.fill(x, y + 4, size, size - 8, color); + } + + @Override + protected void assembleSelf(RenderTarget target) { + + int size = getPreferredSize().x; + int x = getX(); + int y = getY() + (getHeight() - size) / 2; + + // Border + + Vec4 borderColor; + if (RadioButton.this.isPressed() || RadioButton.this.isHovered() || RadioButton.this.isFocused()) { + borderColor = Colors.BLUE; + } else { + borderColor = Colors.LIGHT_GRAY; + } + cross(target, x, y, size, borderColor); + + // Inside area + + if (RadioButton.this.isPressed()) { + // Do nothing + } else { + Vec4 backgroundColor; + if (RadioButton.this.isHovered() && RadioButton.this.isEnabled()) { + backgroundColor = Colors.HOVER_BLUE; + } else { + backgroundColor = Colors.WHITE; + } + cross(target, x + 2, y + 2, size - 4, backgroundColor); + } + + // "Tick" + + if (RadioButton.this.isChecked()) { + cross(target, x + 4, y + 4, size - 8, Colors.BLUE); + } + } + + } + + private boolean checked; + + private RadioButtonGroup group = null; + + public RadioButton(String name, String label, Font labelFont, boolean check) { + super(name, label, labelFont); + this.checked = check; + + assert getChildren().size() == 1 : "RadioButton expects that BasicButton contains exactly one child"; + Component basicChild = getChild(0); + + Panel panel = new Panel(getName() + ".LabelAndTick", new LayoutHorizontal(0, 10)); + removeChild(basicChild); + setLayout(new LayoutAlign(0, 0.5f, 10)); + panel.setLayoutHint(basicChild.getLayoutHint()); + panel.addChild(new Tick()); + panel.addChild(basicChild); + addChild(panel); + + addListener(KeyEvent.class, e -> { + if (e.isRelease()) return false; + + if (e.getKey() == GLFW.GLFW_KEY_LEFT || e.getKey() == GLFW.GLFW_KEY_UP) { + if (group != null) { + group.selectPrevious(); + group.getSelected().takeFocus(); + } + + return true; + } else if (e.getKey() == GLFW.GLFW_KEY_RIGHT || e.getKey() == GLFW.GLFW_KEY_DOWN) { + if (group != null) { + group.selectNext(); + group.getSelected().takeFocus(); + } + return true; + } + + return false; + }); + + addAction(b -> setChecked(true)); + } + + public RadioButton(String name, String label, Font labelFont) { + this(name, label, labelFont, false); + } + + public RadioButton(String name, String label, boolean check) { + this(name, label, new Font(), check); + } + + public RadioButton(String name, String label) { + this(name, label, false); + } + + /** + * @param group the group to set + */ + public RadioButton setGroup(RadioButtonGroup group) { + + if (this.group != null) { + group.selectNext(); + removeAction(group.listener); + group.buttons.remove(this); + group.getSelected(); // Clear reference if this was the only button in the group + } + + this.group = group; + + if (this.group != null) { + group.buttons.add(this); + addAction(group.listener); + } + + setChecked(false); + + return this; + } + + /** + * @return the checked + */ + public boolean isChecked() { + return checked; + } + + /** + * @param checked the checked to set + */ + public void setChecked(boolean checked) { + this.checked = checked; + + if (group != null) { + group.listener.accept(this); // Failsafe for manual invocations of setChecked() + } + } + + @Override + protected void assembleSelf(RenderTarget target) { + // Change label font color + + if (isPressed()) { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLUE)); + } else { + getLabel().setFont(getLabel().getFont().withColor(Colors.BLACK)); + } + } + + @Override + protected void postAssembleSelf(RenderTarget target) { + // Apply disable tint + + if (!isEnabled()) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.toVector(0x88FFFFFF)); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButtonGroup.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButtonGroup.java new file mode 100644 index 0000000..3887018 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButtonGroup.java @@ -0,0 +1,119 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.client.graphics.gui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public class RadioButtonGroup { + + private final Collection> actions = Collections.synchronizedCollection(new ArrayList<>()); + final List buttons = Collections.synchronizedList(new ArrayList<>()); + + private RadioButton selected = null; + + Consumer listener = b -> { + if (b instanceof RadioButton && ((RadioButton) b).isChecked() && buttons.contains(b)) { + select((RadioButton) b); + } + }; + + public RadioButtonGroup addAction(Consumer action) { + this.actions.add(Objects.requireNonNull(action, "action")); + return this; + } + + public boolean removeAction(Consumer action) { + return this.actions.remove(action); + } + + public List getButtons() { + return Collections.unmodifiableList(buttons); + } + + public synchronized RadioButton getSelected() { + if (!buttons.contains(selected)) { + selected = null; + } + return selected; + } + + public synchronized void select(RadioButton button) { + if (button != null && !buttons.contains(button)) { + throw new IllegalArgumentException("Button " + button + " is not in the group"); + } + + getSelected(); // Clear if invalid + + if (selected == button) { + return; // Terminate listener-setter recursion + } + + if (selected != null) { + selected.setChecked(false); + } + + selected = button; + + if (selected != null) { + selected.setChecked(true); + } + + actions.forEach(action -> action.accept(this)); + } + + public void selectNext() { + selectNeighbour(+1); + } + + public void selectPrevious() { + selectNeighbour(-1); + } + + private synchronized void selectNeighbour(int direction) { + if (getSelected() == null) { + if (buttons.isEmpty()) { + throw new IllegalStateException("Cannot select neighbour button: group empty"); + } + + select(buttons.get(0)); + } else { + RadioButton button; + int index = buttons.indexOf(selected); + + do { + index += direction; + + if (index >= buttons.size()) { + index = 0; + } else if (index < 0) { + index = buttons.size() - 1; + } + + button = buttons.get(index); + } while (button != getSelected() && !button.isEnabled()); + + select(button); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/ButtonEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/ButtonEvent.java new file mode 100644 index 0000000..071f06e --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/ButtonEvent.java @@ -0,0 +1,60 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.client.graphics.gui.event; + +import ru.windcorp.progressia.client.graphics.gui.BasicButton; + +public class ButtonEvent extends ComponentEvent { + + public static class Press extends ButtonEvent { + public Press(BasicButton button) { + super(button, true); + } + } + + public static class Release extends ButtonEvent { + public Release(BasicButton button) { + super(button, false); + } + } + + private final boolean isPress; + + protected ButtonEvent(BasicButton button, boolean isPress) { + super(button); + this.isPress = isPress; + } + + public static ButtonEvent create(BasicButton button, boolean isPress) { + if (isPress) { + return new Press(button); + } else { + return new Release(button); + } + } + + public boolean isPress() { + return isPress; + } + + public boolean isRelease() { + return !isPress; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/EnableEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/EnableEvent.java new file mode 100644 index 0000000..f56df2c --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/EnableEvent.java @@ -0,0 +1,11 @@ +package ru.windcorp.progressia.client.graphics.gui.event; + +import ru.windcorp.progressia.client.graphics.gui.Component; + +public class EnableEvent extends ComponentEvent { + + public EnableEvent(Component component) { + super(component); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java index fa2cdfe..a164378 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/layout/LayoutGrid.java @@ -97,16 +97,27 @@ public class LayoutGrid implements Layout { void setBounds(int column, int row, Component child, Component parent) { if (!isSummed) throw new IllegalStateException("Not summed yet"); + + int width, height; + + if (column == columns.length - 1) { + width = parent.getWidth() - margin - columns[column]; + } else { + width = columns[column + 1] - columns[column] - gap; + } + + if (row == rows.length - 1) { + height = parent.getHeight() - margin - rows[row]; + } else { + height = rows[row + 1] - rows[row] - gap; + } child.setBounds( parent.getX() + columns[column], - parent.getY() + rows[row], + parent.getY() + parent.getHeight() - (rows[row] + height), - (column != (columns.length - 1) ? (columns[column + 1] - columns[column] - gap) - : (parent.getWidth() - margin - columns[column])), - - (row != (rows.length - 1) ? (rows[row + 1] - rows[row] - gap) - : (parent.getHeight() - margin - rows[row])) + width, + height ); } } @@ -132,10 +143,9 @@ public class LayoutGrid implements Layout { GridDimensions grid = calculateGrid(c); grid.sum(); - int[] coords; for (Component child : c.getChildren()) { - coords = (int[]) child.getLayoutHint(); - grid.setBounds(coords[0], coords[1], child, c); + Vec2i coords = (Vec2i) child.getLayoutHint(); + grid.setBounds(coords.x, coords.y, child, c); } } } @@ -149,11 +159,10 @@ public class LayoutGrid implements Layout { private GridDimensions calculateGrid(Component parent) { GridDimensions result = new GridDimensions(); - int[] coords; for (Component child : parent.getChildren()) { - coords = (int[]) child.getLayoutHint(); - result.add(coords[0], coords[1], child.getPreferredSize()); + Vec2i coords = (Vec2i) child.getLayoutHint(); + result.add(coords.x, coords.y, child.getPreferredSize()); } return result; diff --git a/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java b/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java new file mode 100644 index 0000000..5e413e2 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java @@ -0,0 +1,104 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.test; + +import org.lwjgl.glfw.GLFW; + +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.GUI; +import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend; +import ru.windcorp.progressia.client.graphics.flat.RenderTarget; +import ru.windcorp.progressia.client.graphics.gui.Button; +import ru.windcorp.progressia.client.graphics.gui.Checkbox; +import ru.windcorp.progressia.client.graphics.gui.GUILayer; +import ru.windcorp.progressia.client.graphics.gui.Panel; +import ru.windcorp.progressia.client.graphics.gui.RadioButton; +import ru.windcorp.progressia.client.graphics.gui.RadioButtonGroup; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutBorderHorizontal; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; +import ru.windcorp.progressia.client.graphics.input.InputEvent; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; +import ru.windcorp.progressia.client.graphics.input.bus.Input; + +public class LayerButtonTest extends GUILayer { + + public LayerButtonTest() { + super("LayerButtonTest", new LayoutBorderHorizontal(0)); + + Panel background = new Panel("Background", new LayoutAlign(10)) { + @Override + protected void assembleSelf(RenderTarget target) { + target.fill(Colors.toVector(0x88FFFFFF)); + } + }; + + Panel panel = new Panel("Panel", new LayoutVertical(10)) { + @Override + protected void assembleSelf(RenderTarget target) { + target.fill(getX(), getY(), getWidth(), getHeight(), Colors.LIGHT_GRAY); + target.fill(getX() + 2, getY() + 2, getWidth() - 4, getHeight() - 4, Colors.WHITE); + } + }; + + Button blockableButton; + panel.addChild((blockableButton = new Button("BlockableButton", "Blockable")).addAction(b -> { + System.out.println("Button Blockable!"); + })); + blockableButton.setEnabled(false); + + panel.addChild(new Checkbox("EnableButton", "Enable").addAction(b -> { + blockableButton.setEnabled(((Checkbox) b).isChecked()); + })); + + RadioButtonGroup group = new RadioButtonGroup().addAction(g -> { + System.out.println("RBG! " + g.getSelected().getLabel().getCurrentText()); + }); + + panel.addChild(new RadioButton("RB1", "Moon").setGroup(group)); + panel.addChild(new RadioButton("RB2", "Type").setGroup(group)); + panel.addChild(new RadioButton("RB3", "Ice").setGroup(group)); + panel.addChild(new RadioButton("RB4", "Cream").setGroup(group)); + + panel.getChild(panel.getChildren().size() - 1).setEnabled(false); + + panel.getChild(1).takeFocus(); + + background.addChild(panel); + getRoot().addChild(background.setLayoutHint(LayoutBorderHorizontal.CENTER)); + } + + @Override + protected void handleInput(Input input) { + + if (!input.isConsumed()) { + + InputEvent e = input.getEvent(); + + if ((e instanceof KeyEvent) && ((KeyEvent) e).isPress() && ((KeyEvent) e).getKey() == GLFW.GLFW_KEY_ESCAPE) { + GUI.removeLayer(this); + GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); + } + + } + + super.handleInput(input); + input.consume(); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java b/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java index f129255..e73dcb4 100644 --- a/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java +++ b/src/main/java/ru/windcorp/progressia/test/TestPlayerControls.java @@ -184,6 +184,7 @@ public class TestPlayerControls { case GLFW.GLFW_KEY_ESCAPE: if (!event.isPress()) return false; + handleEscape(); break; @@ -293,13 +294,20 @@ public class TestPlayerControls { } private void handleEscape() { - if (captureMouse) { - GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); - } else { - GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); - } +// if (captureMouse) { +// GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); +// } else { +// GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED); +// } +// +// captureMouse = !captureMouse; - captureMouse = !captureMouse; + movementForward = 0; + movementRight = 0; + movementUp = 0; + GLFW.glfwSetInputMode(GraphicsBackend.getWindowHandle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL); + GUI.addTopLayer(new LayerButtonTest()); + updateGUI(); }