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();
}