diff --git a/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java b/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java index 2c0d61d..2cc73f4 100644 --- a/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java +++ b/src/main/java/ru/windcorp/progressia/client/comms/controls/ControlTriggers.java @@ -143,20 +143,6 @@ public class ControlTriggers { ); } - // - // - /// - /// - // - // - // - // - // - // - // - // - // - public static ControlTriggerInputBased localOf( String id, Consumer action, diff --git a/src/main/java/ru/windcorp/progressia/client/comms/controls/InputBasedControls.java b/src/main/java/ru/windcorp/progressia/client/comms/controls/InputBasedControls.java index 28b9e1b..1eea281 100644 --- a/src/main/java/ru/windcorp/progressia/client/comms/controls/InputBasedControls.java +++ b/src/main/java/ru/windcorp/progressia/client/comms/controls/InputBasedControls.java @@ -19,7 +19,7 @@ package ru.windcorp.progressia.client.comms.controls; import ru.windcorp.progressia.client.Client; -import ru.windcorp.progressia.client.graphics.input.bus.Input; +import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.common.comms.packets.Packet; public class InputBasedControls { @@ -36,12 +36,12 @@ public class InputBasedControls { .toArray(ControlTriggerInputBased[]::new); } - public void handleInput(Input input) { + public void handleInput(InputEvent event) { for (ControlTriggerInputBased c : controls) { - Packet packet = c.onInputEvent(input.getEvent()); + Packet packet = c.onInputEvent(event); if (packet != null) { - input.consume(); + event.consume(); client.getComms().sendPacket(packet); break; } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java b/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java index 59b89da..5834380 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/GUI.java @@ -23,15 +23,8 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import com.google.common.eventbus.Subscribe; - import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; -import ru.windcorp.progressia.client.graphics.input.CursorEvent; -import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent; -import ru.windcorp.progressia.client.graphics.input.KeyEvent; -import ru.windcorp.progressia.client.graphics.input.WheelEvent; -import ru.windcorp.progressia.client.graphics.input.bus.Input; public class GUI { @@ -46,15 +39,6 @@ public class GUI { private static final List MODIFICATION_QUEUE = Collections .synchronizedList(new ArrayList<>()); - private static class ModifiableInput extends Input { - @Override - public void initialize(InputEvent event, Target target) { - super.initialize(event, target); - } - } - - private static final ModifiableInput THE_INPUT = new ModifiableInput(); - private GUI() { } @@ -126,43 +110,12 @@ public class GUI { LAYERS.forEach(Layer::invalidate); } - private static void dispatchInputEvent(InputEvent event) { - Input.Target target; - - if (event instanceof KeyEvent) { - if (((KeyEvent) event).isMouse()) { - target = Input.Target.HOVERED; - } else { - target = Input.Target.FOCUSED; + public static void dispatchInput(InputEvent event) { + synchronized (LAYERS) { + for (int i = 0; i < LAYERS.size(); ++i) { + LAYERS.get(i).handleInput(event); } - } else if (event instanceof CursorEvent) { - target = Input.Target.HOVERED; - } else if (event instanceof WheelEvent) { - target = Input.Target.HOVERED; - } else if (event instanceof FrameResizeEvent) { - return; - } else { - target = Input.Target.ALL; } - - THE_INPUT.initialize(event, target); - LAYERS.forEach(l -> l.handleInput(THE_INPUT)); - } - - public static Object getEventSubscriber() { - return new Object() { - - @Subscribe - public void onFrameResized(FrameResizeEvent event) { - GUI.invalidateEverything(); - } - - @Subscribe - public void onInput(InputEvent event) { - dispatchInputEvent(event); - } - - }; } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java b/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java index dfa72d5..ad685e4 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/Layer.java @@ -21,7 +21,7 @@ package ru.windcorp.progressia.client.graphics; import java.util.concurrent.atomic.AtomicBoolean; import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; -import ru.windcorp.progressia.client.graphics.input.bus.Input; +import ru.windcorp.progressia.client.graphics.input.InputEvent; public abstract class Layer { @@ -106,7 +106,7 @@ public abstract class Layer { protected abstract void doRender(); - protected abstract void handleInput(Input input); + public abstract void handleInput(InputEvent input); protected int getWidth() { return GraphicsInterface.getFrameWidth(); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java index ffd0b49..6f59d39 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/GraphicsInterface.java @@ -68,6 +68,10 @@ public class GraphicsInterface { public static void subscribeToInputEvents(Object listener) { InputHandler.register(listener); } + + public static void unsubscribeFromInputEvents(Object listener) { + InputHandler.unregister(listener); + } public static void startNextLayer() { GraphicsBackend.startNextLayer(); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/InputHandler.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/InputHandler.java index 34d936c..560ccaf 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/InputHandler.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/InputHandler.java @@ -39,6 +39,7 @@ public class InputHandler { public void initialize(int key, int scancode, int action, int mods) { this.setTime(GraphicsInterface.getTime()); + this.setConsumed(false); this.key = key; this.scancode = scancode; this.action = action; @@ -59,7 +60,7 @@ public class InputHandler { if (GraphicsBackend.getWindowHandle() != window) return; THE_KEY_EVENT.initialize(key, scancode, action, mods); - dispatch(THE_KEY_EVENT); + INPUT_EVENT_BUS.post(THE_KEY_EVENT); switch (action) { case GLFW.GLFW_PRESS: @@ -90,6 +91,7 @@ public class InputHandler { public void initialize(double x, double y) { this.setTime(GraphicsInterface.getTime()); + this.setConsumed(false); getNewPosition().set(x, y); } @@ -109,7 +111,7 @@ public class InputHandler { InputTracker.initializeCursorPosition(x, y); THE_CURSOR_MOVE_EVENT.initialize(x, y); - dispatch(THE_CURSOR_MOVE_EVENT); + INPUT_EVENT_BUS.post(THE_CURSOR_MOVE_EVENT); InputTracker.getCursorPosition().set(x, y); } @@ -124,6 +126,7 @@ public class InputHandler { public void initialize(double xOffset, double yOffset) { this.setTime(GraphicsInterface.getTime()); + this.setConsumed(false); this.getOffset().set(xOffset, yOffset); } @@ -139,7 +142,7 @@ public class InputHandler { if (GraphicsBackend.getWindowHandle() != window) return; THE_WHEEL_SCROLL_EVENT.initialize(xoffset, yoffset); - dispatch(THE_WHEEL_SCROLL_EVENT); + INPUT_EVENT_BUS.post(THE_WHEEL_SCROLL_EVENT); } // FrameResizeEvent @@ -152,6 +155,7 @@ public class InputHandler { public void initialize(int width, int height) { this.setTime(GraphicsInterface.getTime()); + this.setConsumed(false); this.getNewSize().set(width, height); } @@ -167,17 +171,17 @@ public class InputHandler { int height ) { THE_FRAME_RESIZE_EVENT.initialize(width, height); - dispatch(THE_FRAME_RESIZE_EVENT); + INPUT_EVENT_BUS.post(THE_FRAME_RESIZE_EVENT); } // Misc - private static void dispatch(InputEvent event) { - INPUT_EVENT_BUS.post(event); - } - public static void register(Object listener) { INPUT_EVENT_BUS.register(listener); } + + public static void unregister(Object listener) { + INPUT_EVENT_BUS.unregister(listener); + } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java b/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java index 9239150..9bc280e 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/backend/LWJGLInitializer.java @@ -24,7 +24,11 @@ import static org.lwjgl.system.MemoryUtil.*; import org.lwjgl.opengl.GL; +import com.google.common.eventbus.Subscribe; + import ru.windcorp.progressia.client.graphics.GUI; +import ru.windcorp.progressia.client.graphics.input.FrameResizeEvent; +import ru.windcorp.progressia.client.graphics.input.InputEvent; class LWJGLInitializer { @@ -107,7 +111,20 @@ class LWJGLInitializer { glfwSetScrollCallback(handle, InputHandler::handleWheelScroll); - GraphicsInterface.subscribeToInputEvents(GUI.getEventSubscriber()); + GraphicsInterface.subscribeToInputEvents(new Object() { + + @Subscribe + public void onFrameResized(FrameResizeEvent event) { + GUI.invalidateEverything(); + } + + @Subscribe + public void onInputEvent(InputEvent event) { + GUI.dispatchInput(event); + } + + }); + } } 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 index 6b86627..20f1ef4 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/BasicButton.java @@ -54,18 +54,17 @@ public abstract class BasicButton extends Component { reassembleAt(ARTrigger.HOVER, ARTrigger.FOCUS, ARTrigger.ENABLE); // Click triggers - addListener(KeyEvent.class, e -> { - if (e.isRepeat()) { - return false; - } else if ( + addInputListener(KeyEvent.class, e -> { + if (e.isRepeat()) + return; + + if ( e.isLeftMouseButton() || e.getKey() == GLFW.GLFW_KEY_SPACE || e.getKey() == GLFW.GLFW_KEY_ENTER ) { setPressed(e.isPress()); - return true; - } else { - return false; + e.consume(); } }); 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 8f1aa9a..417c6ea 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 @@ -25,8 +25,6 @@ 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; @@ -39,9 +37,10 @@ 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; +import ru.windcorp.progressia.client.graphics.input.CursorMoveEvent; 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; +import ru.windcorp.progressia.client.graphics.input.KeyMatcher; import ru.windcorp.progressia.client.graphics.input.bus.InputBus; import ru.windcorp.progressia.client.graphics.input.bus.InputListener; import ru.windcorp.progressia.common.util.Named; @@ -55,7 +54,7 @@ public class Component extends Named { private Component parent = null; private EventBus eventBus = null; - private InputBus inputBus = null; + private final InputBus inputBus = new InputBus(this); private int x, y; private int width, height; @@ -76,6 +75,9 @@ public class Component extends Named { public Component(String name) { super(name); + + // Update hover flag when cursor moves + addInputListener(CursorMoveEvent.class, this::updateHoverFlag, InputBus.Option.ALWAYS); } public Component getParent() { @@ -521,6 +523,10 @@ public class Component extends Named { dispatchEvent(new HoverEvent(this, isHovered)); } } + + private void updateHoverFlag(CursorMoveEvent e) { + setHovered(contains((int) InputTracker.getCursorX(), (int) InputTracker.getCursorY())); + } public void addListener(Object listener) { if (eventBus == null) { @@ -542,121 +548,28 @@ public class Component extends Named { eventBus.post(event); } - public void addListener( - Class type, - boolean handlesConsumed, - InputListener listener - ) { - if (inputBus == null) { - inputBus = new InputBus(); - } - - inputBus.register(type, handlesConsumed, listener); + public void addInputListener(Class type, InputListener listener, InputBus.Option... options) { + inputBus.register(type, listener, options); + } + + public void addKeyListener(KeyMatcher matcher, InputListener listener, InputBus.Option... options) { + inputBus.register(matcher, listener, options); } - public void addListener(Class type, InputListener listener) { - if (inputBus == null) { - inputBus = new InputBus(); - } - - inputBus.register(type, listener); - } - - public void removeListener(InputListener listener) { + public void removeInputListener(InputListener listener) { if (inputBus != null) { inputBus.unregister(listener); } } - - protected void handleInput(Input input) { - if (inputBus != null && isEnabled()) { - inputBus.dispatch(input); - } + + InputBus getInputBus() { + return inputBus; } - - public void dispatchInput(Input input) { - try { - switch (input.getTarget()) { - case FOCUSED: - dispatchInputToFocused(input); - break; - case HOVERED: - dispatchInputToHovered(input); - break; - case ALL: - default: - dispatchInputToAll(input); - break; - } - } catch (Exception e) { - throw CrashReports.report(e, "Could not dispatch input to Component %s", this); - } - } - - private void dispatchInputToFocused(Input input) { - Component c = findFocused(); - - if (c == null) - return; - if (attemptFocusTransfer(input, c)) - return; - - while (c != null) { - c.handleInput(input); - c = c.getParent(); - } - } - - private void dispatchInputToHovered(Input input) { - getChildren().forEach(child -> { - if (child.containsCursor()) { - child.setHovered(true); - - if (!input.isConsumed()) { - child.dispatchInput(input); - } - } else { - child.setHovered(false); - } - }); - - handleInput(input); - } - - private void dispatchInputToAll(Input input) { - getChildren().forEach(c -> c.dispatchInput(input)); - handleInput(input); - } - - private boolean attemptFocusTransfer(Input input, Component focused) { - if (input.isConsumed()) - return false; - if (!(input.getEvent() instanceof KeyEvent)) - return false; - - KeyEvent keyInput = (KeyEvent) input.getEvent(); - - if (keyInput.getKey() == GLFW.GLFW_KEY_TAB && !keyInput.isRelease()) { - input.consume(); - if (keyInput.hasShift()) { - focused.focusPrevious(); - } else { - focused.focusNext(); - } - return true; - } - - return false; - } - + public synchronized boolean contains(int x, int y) { return x >= getX() && x < getX() + getWidth() && y >= getY() && y < getY() + getHeight(); } - public boolean containsCursor() { - return contains((int) InputTracker.getCursorX(), (int) InputTracker.getCursorY()); - } - public void requestReassembly() { if (parent != null) { parent.requestReassembly(); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/DragManager.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/DragManager.java new file mode 100644 index 0000000..462be26 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/DragManager.java @@ -0,0 +1,77 @@ +/* + * 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.Objects; + +import glm.vec._2.d.Vec2d; +import ru.windcorp.progressia.client.graphics.gui.event.DragEvent; +import ru.windcorp.progressia.client.graphics.gui.event.DragStartEvent; +import ru.windcorp.progressia.client.graphics.gui.event.DragStopEvent; +import ru.windcorp.progressia.client.graphics.input.CursorMoveEvent; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; +import ru.windcorp.progressia.client.graphics.input.KeyMatcher; +import ru.windcorp.progressia.client.graphics.input.bus.InputBus; + +public class DragManager { + + private Component component; + + private boolean isDragged = false; + private final Vec2d change = new Vec2d(); + + public void install(Component c) { + Objects.requireNonNull(c, "c"); + if (c == component) { + return; + } + if (component != null) { + throw new IllegalStateException("Already installed on " + component + "; attempted to install on " + c); + } + + component = c; + + c.addInputListener(CursorMoveEvent.class, this::onCursorMove, InputBus.Option.ALWAYS); + c.addKeyListener(KeyMatcher.LMB, this::onLMB, InputBus.Option.ALWAYS, InputBus.Option.IGNORE_ACTION); + } + + private void onCursorMove(CursorMoveEvent e) { + if (isDragged) { + Vec2d currentChange = e.getChange(null); + change.add(currentChange); + component.dispatchEvent(new DragEvent(component, currentChange, change)); + } + } + + private void onLMB(KeyEvent e) { + if (isDragged && e.isRelease()) { + + isDragged = false; + component.dispatchEvent(new DragStopEvent(component, change)); + + } else if (!isDragged && !e.isConsumed() && e.isPress() && component.isHovered()) { + + isDragged = true; + change.set(0, 0); + component.dispatchEvent(new DragStartEvent(component)); + e.consume(); + + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/GUILayer.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/GUILayer.java index 2e0981c..b808aed 100755 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/GUILayer.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/GUILayer.java @@ -15,12 +15,19 @@ * 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.Iterator; + +import org.lwjgl.glfw.GLFW; + import ru.windcorp.progressia.client.graphics.flat.AssembledFlatLayer; import ru.windcorp.progressia.client.graphics.flat.RenderTarget; -import ru.windcorp.progressia.client.graphics.input.bus.Input; +import ru.windcorp.progressia.client.graphics.input.InputEvent; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; +import ru.windcorp.progressia.client.graphics.input.bus.InputBus; +import ru.windcorp.progressia.common.util.StashingStack; public abstract class GUILayer extends AssembledFlatLayer { @@ -33,7 +40,9 @@ public abstract class GUILayer extends AssembledFlatLayer { public GUILayer(String name, Layout layout) { super(name); + getRoot().setLayout(layout); + getRoot().addInputListener(KeyEvent.class, this::attemptFocusTransfer, InputBus.Option.IGNORE_FOCUS); } public Component getRoot() { @@ -47,9 +56,81 @@ public abstract class GUILayer extends AssembledFlatLayer { getRoot().assemble(target); } + /** + * Stack frame for {@link #handleInput(InputEvent)}. + */ + private static class EventHandlingFrame { + Component component; + Iterator children; + + void init(Component c) { + component = c; + children = c.getChildren().iterator(); + } + + void reset() { + component = null; + children = null; + } + } + + /** + * Stack for {@link #handleInput(InputEvent)}. + */ + private StashingStack path = new StashingStack<>(64, EventHandlingFrame::new); + + /* + * This is essentially a depth-first iteration of the component tree. The + * recursive procedure has been unrolled to reduce call stack length. + */ @Override - protected void handleInput(Input input) { - getRoot().dispatchInput(input); + public void handleInput(InputEvent event) { + if (!path.isEmpty()) { + throw new IllegalStateException( + "path is not empty: " + path + ". Are events being processed concurrently?" + ); + } + + path.push().init(root); + + while (!path.isEmpty()) { + + Iterator it = path.peek().children; + if (it.hasNext()) { + + Component c = it.next(); + + if (c.isEnabled()) { + if (c.getChildren().isEmpty()) { + c.getInputBus().dispatch(event); + } else { + path.push().init(c); + } + } + + } else { + path.peek().component.getInputBus().dispatch(event); + path.pop().reset(); + } + + } + } + + private void attemptFocusTransfer(KeyEvent e) { + Component focused = getRoot().findFocused(); + + if (focused == null) { + return; + } + + if (e.getKey() == GLFW.GLFW_KEY_TAB && !e.isRelease()) { + e.consume(); + if (e.hasShift()) { + focused.focusPrevious(); + } else { + focused.focusNext(); + } + } } @Override 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 index 1ee7f66..8ea76f3 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/RadioButton.java @@ -104,26 +104,22 @@ public class RadioButton extends BasicButton { group.addChild(basicChild); addChild(group); - addListener(KeyEvent.class, e -> { - if (e.isRelease()) - return false; + addInputListener(KeyEvent.class, e -> { + if (e.isRelease()) return; if (e.getKey() == GLFW.GLFW_KEY_LEFT || e.getKey() == GLFW.GLFW_KEY_UP) { if (this.group != null) { this.group.selectPrevious(); this.group.getSelected().takeFocus(); } - - return true; + e.consume(); } else if (e.getKey() == GLFW.GLFW_KEY_RIGHT || e.getKey() == GLFW.GLFW_KEY_DOWN) { if (this.group != null) { this.group.selectNext(); this.group.getSelected().takeFocus(); } - return true; + e.consume(); } - - return false; }); addAction(b -> setChecked(true)); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragEvent.java new file mode 100644 index 0000000..74c91ae --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragEvent.java @@ -0,0 +1,58 @@ +/* + * 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 glm.vec._2.d.Vec2d; +import ru.windcorp.progressia.client.graphics.gui.Component; + +public class DragEvent extends ComponentEvent { + + private final Vec2d currentChange = new Vec2d(); + private final Vec2d totalChange = new Vec2d(); + + public DragEvent(Component component, Vec2d currentChange, Vec2d totalChange) { + super(component); + this.currentChange.set(currentChange.x, currentChange.y); + this.totalChange.set(totalChange.x, totalChange.y); + } + + public Vec2d getCurrentChange() { + return currentChange; + } + + public double getCurrentChangeX() { + return currentChange.x; + } + + public double getCurrentChangeY() { + return currentChange.y; + } + + public Vec2d getTotalChange() { + return totalChange; + } + + public double getTotalChangeX() { + return totalChange.x; + } + + public double getTotalChangeY() { + return totalChange.y; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragStartEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragStartEvent.java new file mode 100644 index 0000000..232b02a --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragStartEvent.java @@ -0,0 +1,28 @@ +/* + * 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.Component; + +public class DragStartEvent extends ComponentEvent { + + public DragStartEvent(Component component) { + super(component); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/Input.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragStopEvent.java similarity index 51% rename from src/main/java/ru/windcorp/progressia/client/graphics/input/bus/Input.java rename to src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragStopEvent.java index 1aad6bb..bc23628 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/Input.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/event/DragStopEvent.java @@ -15,48 +15,30 @@ * 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.input.bus; +package ru.windcorp.progressia.client.graphics.gui.event; -import ru.windcorp.progressia.client.graphics.input.InputEvent; +import glm.vec._2.d.Vec2d; +import ru.windcorp.progressia.client.graphics.gui.Component; -public class Input { +public class DragStopEvent extends ComponentEvent { - public static enum Target { - FOCUSED, HOVERED, ALL + private final Vec2d totalChange = new Vec2d(); + + public DragStopEvent(Component component, Vec2d totalChange) { + super(component); + this.totalChange.set(totalChange.x, totalChange.y); } - - private InputEvent event; - - private boolean isConsumed; - - private Target target; - - protected void initialize(InputEvent event, Target target) { - this.event = event; - this.target = target; - - this.isConsumed = false; + + public Vec2d getTotalChange() { + return totalChange; } - - public InputEvent getEvent() { - return event; + + public double getTotalChangeX() { + return totalChange.x; } - - public boolean isConsumed() { - return isConsumed; - } - - public void setConsumed(boolean isConsumed) { - this.isConsumed = isConsumed; - } - - public void consume() { - setConsumed(true); - } - - public Target getTarget() { - return target; + + public double getTotalChangeY() { + return totalChange.y; } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java b/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java index 4fa155c..2c42cf0 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/gui/menu/MenuLayer.java @@ -33,7 +33,6 @@ import ru.windcorp.progressia.client.graphics.gui.layout.LayoutFill; 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; import ru.windcorp.progressia.client.localization.MutableString; import ru.windcorp.progressia.client.localization.MutableStringLocalized; @@ -97,11 +96,9 @@ public class MenuLayer extends GUILayer { } @Override - protected void handleInput(Input input) { + public void handleInput(InputEvent event) { - if (!input.isConsumed()) { - InputEvent event = input.getEvent(); - + if (!event.isConsumed()) { if (event instanceof KeyEvent) { KeyEvent keyEvent = (KeyEvent) event; if (keyEvent.isPress() && keyEvent.getKey() == GLFW.GLFW_KEY_ESCAPE) { @@ -110,8 +107,8 @@ public class MenuLayer extends GUILayer { } } - super.handleInput(input); - input.consume(); + super.handleInput(event); + event.consume(); } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/CursorMoveEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/input/CursorMoveEvent.java index c87fc62..ff7ea82 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/CursorMoveEvent.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/input/CursorMoveEvent.java @@ -18,7 +18,6 @@ package ru.windcorp.progressia.client.graphics.input; -import glm.vec._2.Vec2; import glm.vec._2.d.Vec2d; public class CursorMoveEvent extends CursorEvent { @@ -81,7 +80,10 @@ public class CursorMoveEvent extends CursorEvent { return getNewY() - getPreviousY(); } - public Vec2 getChange(Vec2 result) { + public Vec2d getChange(Vec2d result) { + if (result == null) { + result = new Vec2d(); + } return result.set(getChangeX(), getChangeY()); } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/InputEvent.java b/src/main/java/ru/windcorp/progressia/client/graphics/input/InputEvent.java index b0c2483..5edf055 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/InputEvent.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/input/InputEvent.java @@ -15,13 +15,35 @@ * 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.input; +import ru.windcorp.progressia.client.graphics.gui.Component; + +/** + * An instance of user input. + *

+ * User input events are typically generated by graphics backend between frames + * and passed to the graphics layers from top to bottom. Layers that use + * {@link Component}s will forward this event through the Component hierarchy. + *

+ * Events have a {@code consumed} flag. A freshly-generated event will have this + * flag set to {@code false}. Event listeners that process the event will + * usually choose to raise the flag ("consume the event") to ask future + * listeners to ignore this event. This is done to avoid multiple UI interfaces + * reacting to single input. By default, listeners will not receive consumed + * events; however, some listeners may choose to receive, handle and even + * un-consume the event. + *

+ * {@code InputEvent} objects may be reused for future input events after their + * processing is complete; to obtain a static copy, use {@link #snapshot()}. + */ public abstract class InputEvent { private double time; + private boolean isConsumed = false; + public InputEvent(double time) { this.time = time; } @@ -36,4 +58,16 @@ public abstract class InputEvent { public abstract InputEvent snapshot(); + public boolean isConsumed() { + return isConsumed; + } + + public void setConsumed(boolean isConsumed) { + this.isConsumed = isConsumed; + } + + public void consume() { + setConsumed(true); + } + } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/KeyMatcher.java b/src/main/java/ru/windcorp/progressia/client/graphics/input/KeyMatcher.java index 6fedcd6..048ddb5 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/KeyMatcher.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/input/KeyMatcher.java @@ -18,16 +18,75 @@ package ru.windcorp.progressia.client.graphics.input; -import java.util.function.Predicate; +import java.util.Map; +import java.util.regex.Pattern; import org.lwjgl.glfw.GLFW; +import com.google.common.collect.ImmutableMap; + public class KeyMatcher { + + private static final Pattern DECLAR_SPLIT_REGEX = Pattern.compile("\\s*\\+\\s*"); + private static final Map MOD_TOKENS = ImmutableMap.of( + "SHIFT", GLFW.GLFW_MOD_SHIFT, + "CONTROL", GLFW.GLFW_MOD_CONTROL, + "ALT", GLFW.GLFW_MOD_ALT, + "SUPER", GLFW.GLFW_MOD_SUPER + ); + + public static final KeyMatcher LMB = new KeyMatcher(GLFW.GLFW_MOUSE_BUTTON_LEFT); + public static final KeyMatcher RMB = new KeyMatcher(GLFW.GLFW_MOUSE_BUTTON_RIGHT); + public static final KeyMatcher MMB = new KeyMatcher(GLFW.GLFW_MOUSE_BUTTON_MIDDLE); private final int key; private final int mods; - protected KeyMatcher(int key, int mods) { + public KeyMatcher(int key, int mods) { + this.key = key; + this.mods = mods; + } + + public KeyMatcher(int key) { + this.key = key; + this.mods = 0; + } + + public KeyMatcher(String declar) { + String[] tokens = DECLAR_SPLIT_REGEX.split(declar); + if (tokens.length == 0) { + throw new IllegalArgumentException("No tokens found in \"" + declar + "\""); + } + + int key = -1; + int mods = 0; + + for (String token : tokens) { + token = token.toUpperCase(); + + if (MOD_TOKENS.containsKey(token)) { + int mod = MOD_TOKENS.get(token); + if ((mods & mod) != 0) { + throw new IllegalArgumentException("Duplicate modifier \"" + token + "\" in \"" + declar + "\""); + } + mods |= mod; + } else if (key != -1) { + throw new IllegalArgumentException("Too many non-modifier tokens in \"" + declar + "\": maximum one key, first offender: \"" + token + "\""); + } else { + token = token.replace(' ', '_'); + + if (token.startsWith("KEYPAD_")) { + token = "KP_" + token.substring("KEYPAD_".length()); + } + + key = Keys.getCode(token); + + if (key == -1) { + throw new IllegalArgumentException("Unknown token \"" + token + "\" in \"" + declar + "\""); + } + } + } + this.key = key; this.mods = mods; } @@ -42,6 +101,15 @@ public class KeyMatcher { return true; } + + public boolean matchesIgnoringAction(KeyEvent event) { + if (event.getKey() != getKey()) + return false; + if ((event.getMods() & getMods()) != getMods()) + return false; + + return true; + } public int getKey() { return key; @@ -50,49 +118,25 @@ public class KeyMatcher { public int getMods() { return mods; } - - public static KeyMatcher.Builder of(int key) { - return new KeyMatcher.Builder(key); + + public KeyMatcher with(int modifier) { + return new KeyMatcher(key, mods | modifier); } - public static class Builder { + public KeyMatcher withShift() { + return with(GLFW.GLFW_MOD_SHIFT); + } - private final int key; - private int mods = 0; + public KeyMatcher withCtrl() { + return with(GLFW.GLFW_MOD_CONTROL); + } - public Builder(int key) { - this.key = key; - } - - public Builder with(int modifier) { - this.mods += modifier; - return this; - } - - public Builder withShift() { - return with(GLFW.GLFW_MOD_SHIFT); - } - - public Builder withCtrl() { - return with(GLFW.GLFW_MOD_CONTROL); - } - - public Builder withAlt() { - return with(GLFW.GLFW_MOD_ALT); - } - - public Builder withSuper() { - return with(GLFW.GLFW_MOD_SUPER); - } - - public KeyMatcher build() { - return new KeyMatcher(key, mods); - } - - public Predicate matcher() { - return build()::matches; - } + public KeyMatcher withAlt() { + return with(GLFW.GLFW_MOD_ALT); + } + public KeyMatcher withSuper() { + return with(GLFW.GLFW_MOD_SUPER); } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/Keys.java b/src/main/java/ru/windcorp/progressia/client/graphics/input/Keys.java index 3904f59..9132c37 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/Keys.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/input/Keys.java @@ -139,7 +139,7 @@ public class Keys { } public static int getCode(String internalName) { - if (NAMES_TO_CODES.containsKey(internalName)) { + if (!NAMES_TO_CODES.containsKey(internalName)) { return -1; } else { return NAMES_TO_CODES.get(internalName); diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputBus.java b/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputBus.java index a22a243..82f7275 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputBus.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputBus.java @@ -15,73 +15,401 @@ * 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.input.bus; import java.util.ArrayList; import java.util.Collection; +import java.util.Objects; +import ru.windcorp.jputil.ArrayUtil; +import ru.windcorp.progressia.client.graphics.gui.Component; +import ru.windcorp.progressia.client.graphics.input.CursorEvent; import ru.windcorp.progressia.client.graphics.input.InputEvent; +import ru.windcorp.progressia.client.graphics.input.KeyEvent; +import ru.windcorp.progressia.client.graphics.input.KeyMatcher; +import ru.windcorp.progressia.client.graphics.input.WheelEvent; +import ru.windcorp.progressia.common.util.crash.CrashReports; +/** + * An event bus optionally related to a {@link Component} that delivers input + * events to input listeners. This bus may skip listeners based on circumstance; + * behavior can be customized for each listener with {@link Option}s. + *

+ * By default, events are filtered by four checks before being delivered to each + * listener: + *

    + *
  1. Consumption check: unless {@link Option#RECEIVE_CONSUMED + * RECEIVE_CONSUMED} is set, events that are consumed will not be + * delivered.
  2. + *
  3. Hover check: for certain event types (for example, + * {@link WheelEvent} or {@link KeyEvent} that {@link KeyEvent#isMouse() + * isMouse()}), the event will only be delivered if the component is hovered. + * This check may be bypassed with option {@link Option#IGNORE_HOVER + * IGNORE_HOVER} or made mandatory for all events with + * {@link Option#REQUIRE_HOVER REQUIRE_HOVER}. Hover check automatically + * succeeds if no component is provided.
  4. + *
  5. Focus check: for certain event types (for example, + * {@link KeyEvent} that {@code !isMouse()}), the event will only be delivered + * if the component has focus. This check may be bypassed with option + * {@link Option#IGNORE_FOCUS IGNORE_FOCUS} or made mandatory for all events + * with {@link Option#REQUIRE_FOCUS REQUIRE_FOCUS}. Focus check automatically + * succeeds if no component is provided.
  6. + *
  7. Type check: events of type {@code E} are only delivered to + * listeners registered with event type {@code T} if objects of type {@code E} + * can be cast to {@code T}.
  8. + *
+ * Checks 1-3 are bypassed when option {@link Option#ALWAYS ALWAYS} is + * specified. + */ public class InputBus { - private static class WrappedListener { + /** + * Options that allow customization of checks for listeners. + */ + public enum Option { + + /** + * Ignore checks for consumed events, hover and focus; deliver event if + * at all possible. This is shorthand for {@link #RECEIVE_CONSUMED}, + * {@link #IGNORE_HOVER} and {@link #IGNORE_FOCUS}. + */ + ALWAYS, + + /** + * Receive events that were previously consumed. + */ + RECEIVE_CONSUMED, + + /** + * Do not process events if the listener is registered with a component + * and the component is not hovered. + */ + REQUIRE_HOVER, + + /** + * Deliver events even if the event is limited to hovered components by + * default. + */ + IGNORE_HOVER, + + /** + * Do not process events if the listener is registered with a component + * and the component is not focused. + */ + REQUIRE_FOCUS, + + /** + * Deliver events even if the event is limited to focused components by + * default. + */ + IGNORE_FOCUS, + + /** + * Deliver events according to + * {@link KeyMatcher#matchesIgnoringAction(KeyEvent)} rather than + * {@link KeyMatcher#matches(KeyEvent)} when a {@link KeyMatcher} is + * specified. + */ + IGNORE_ACTION; + + } + + private enum YesNoDefault { + YES, NO, DEFAULT; + } + + /** + * A listener with check preferences resolved and type specified. + */ + private class WrappedListener { private final Class type; - private final boolean handleConsumed; + + private final boolean dropIfConsumed; + private final YesNoDefault dropIfNotHovered; + private final YesNoDefault dropIfNotFocused; + private final InputListener listener; public WrappedListener( Class type, - boolean handleConsumed, + boolean dropIfConsumed, + YesNoDefault dropIfNotHovered, + YesNoDefault dropIfNotFocused, InputListener listener ) { this.type = type; - this.handleConsumed = handleConsumed; + this.dropIfConsumed = dropIfConsumed; + this.dropIfNotHovered = dropIfNotHovered; + this.dropIfNotFocused = dropIfNotFocused; this.listener = listener; } - private boolean handles(Input input) { - return (!input.isConsumed() || handleConsumed) && - type.isInstance(input.getEvent()); + private boolean handles(InputEvent input) { + if (dropIfConsumed && input.isConsumed()) + return false; + + switch (dropIfNotHovered) { + case YES: + if (!isHovered()) + return false; + break; + case NO: + break; + default: + + if (isHovered()) + break; + + if (input instanceof KeyEvent && ((KeyEvent) input).isMouse()) + return false; + + if (input instanceof CursorEvent) + return false; + + if (input instanceof WheelEvent) + return false; + + break; + } + + switch (dropIfNotFocused) { + case YES: + if (!isFocused()) + return false; + break; + case NO: + break; + default: + + if (isFocused()) + break; + + if (input instanceof KeyEvent && !((KeyEvent) input).isMouse()) + return false; + + break; + } + + if (!type.isInstance(input)) + return false; + + return true; } + /** + * Invokes the listener if the event is deemed appropriate by the four + * checks. + * + * @param event the event to deliver + */ @SuppressWarnings("unchecked") - public void handle(Input input) { - if (handles(input)) { - boolean consumed = ((InputListener) listener) - .handle( - (InputEvent) type.cast(input.getEvent()) - ); + public void handle(InputEvent event) { + if (handles(event)) { + // A runtime check of types has been performed; this is safe. + InputListener castListener = (InputListener) listener; - input.setConsumed(consumed); + try { + castListener.handle(event); + } catch (Exception e) { + throw CrashReports.report( + e, + "InputListener %s for component %s has failed to receive event %s", + listener, + owner, + event + ); + } } } } + /** + * The component queried for focus and hover. May be {@code null}. + */ + private final Component owner; + + /** + * Registered listeners. + */ private final Collection listeners = new ArrayList<>(4); - public void dispatch(Input input) { - listeners.forEach(l -> l.handle(input)); + /** + * Creates a new input bus that consults the specified {@link Component} to + * determine hover and focus. + * + * @param owner the component to use for hover and focus tests + * @see #InputBus() + */ + public InputBus(Component owner) { + this.owner = Objects.requireNonNull(owner, "owner"); } + /** + * Creates a new input bus that assumes all hover and focus checks are + * successful. + * + * @see #InputBus(Component) + */ + public InputBus() { + this.owner = null; + } + + /** + * Determines whether hover should be assumed for this event bus. + * + * @return {@code true} iff no component is linked or the linked component + * is hovered + */ + private boolean isHovered() { + return owner == null ? true : owner.isHovered(); + } + + /** + * Determines whether focus should be assumed for this event bus. + * + * @return {@code true} iff no component is linked or the linked component + * is focused + */ + private boolean isFocused() { + return owner == null ? true : owner.isFocused(); + } + + /** + * Dispatches (delivers) the provided event to all appropriate listeners. + * + * @param event the event to process + */ + public void dispatch(InputEvent event) { + Objects.requireNonNull(event, "event"); + for (WrappedListener listener : listeners) { + listener.handle(event); + } + } + + /** + * Registers a listener on this bus. + *

+ * {@code type} specifies the class of events that should be passed to this + * listener. Only events of types that extend, implement or equal + * {@code type} are processed. + *

+ * Zero or more {@link Option}s may be specified to enable or disable the + * processing of certain events in certain circumstances. See + * {@linkplain InputBus class description} for a detailed breakdown of the + * checks performed and the effects of various options. When providing + * options to this method, later options override the effects of previous + * options. + *

+ * Option {@link Option#IGNORE_ACTION IGNORE_ACTION} is ignored silently. + * + * @param type the event class to deliver + * @param listener the listener + * @param options the options for this listener + */ public void register( Class type, - boolean handlesConsumed, - InputListener listener + InputListener listener, + Option... options ) { - listeners.add(new WrappedListener(type, handlesConsumed, listener)); + Objects.requireNonNull(type, "type"); + Objects.requireNonNull(listener, "listener"); + + boolean dropIfConsumed = true; + YesNoDefault dropIfNotHovered = YesNoDefault.DEFAULT; + YesNoDefault dropIfNotFocused = YesNoDefault.DEFAULT; + + if (options != null) { + for (Option option : options) { + switch (option) { + case ALWAYS: + dropIfConsumed = false; + dropIfNotHovered = YesNoDefault.NO; + dropIfNotFocused = YesNoDefault.NO; + break; + case RECEIVE_CONSUMED: + dropIfConsumed = false; + break; + case REQUIRE_HOVER: + dropIfNotHovered = YesNoDefault.YES; + break; + case IGNORE_HOVER: + dropIfNotFocused = YesNoDefault.NO; + break; + case REQUIRE_FOCUS: + dropIfNotHovered = YesNoDefault.YES; + break; + case IGNORE_FOCUS: + dropIfNotFocused = YesNoDefault.NO; + break; + case IGNORE_ACTION: + // Ignore + break; + default: + throw new IllegalArgumentException("Unexpected option " + option); + } + } + } + + listeners.add(new WrappedListener(type, dropIfConsumed, dropIfNotHovered, dropIfNotFocused, listener)); } - public void register( - Class type, - InputListener listener - ) { - register(type, false, listener); + /** + * Registers a {@link KeyEvent} listener on this bus. An event has to match + * the provided {@link KeyMatcher} to be delivered to the listener. + *

+ * Zero or more {@link Option}s may be specified to enable or disable the + * processing of certain events in certain circumstances. See + * {@linkplain InputBus class description} for a detailed breakdown of the + * checks performed and the effects of various options. When providing + * options to this method, later options override the effects of previous + * options. + *

+ * Option {@link Option#IGNORE_ACTION IGNORE_ACTION} requests that events + * are delivered according to + * {@link KeyMatcher#matchesIgnoringAction(KeyEvent)} rather than + * {@link KeyMatcher#matches(KeyEvent)}. + * specified. + * + * @param matcher an event filter + * @param listener the listener + * @param options the options for this listener + */ + public void register(KeyMatcher matcher, InputListener listener, Option... options) { + Objects.requireNonNull(matcher, "matcher"); + Objects.requireNonNull(listener, "listener"); + + InputListener filteringListener; + + if (ArrayUtil.firstIndexOf(options, Option.IGNORE_ACTION) != -1) { + filteringListener = e -> { + if (matcher.matchesIgnoringAction(e)) { + listener.handle(e); + } + }; + } else { + filteringListener = e -> { + if (matcher.matches(e)) { + listener.handle(e); + } + }; + } + + register(KeyEvent.class, filteringListener, options); } + /** + * Removes all occurrences of the provided listener from this bus. + * + * @param listener the listener to unregister + */ public void unregister(InputListener listener) { + if (listener == null) { + return; + } + listeners.removeIf(l -> l.listener == listener); } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputListener.java b/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputListener.java index 0d68b5e..1db2410 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputListener.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/input/bus/InputListener.java @@ -23,6 +23,6 @@ import ru.windcorp.progressia.client.graphics.input.InputEvent; @FunctionalInterface public interface InputListener { - boolean handle(T event); + void handle(T event); } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/EntityAnchor.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/EntityAnchor.java index cb578d9..1df46cf 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/EntityAnchor.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/EntityAnchor.java @@ -91,5 +91,14 @@ public class EntityAnchor implements Anchor { public Collection getCameraModes() { return modes; } + + public EntityData getEntity() { + return entity; + } + + @Override + public String toString() { + return "Anchor for entity " + entity; + } } diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java b/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java index c81e7a7..df3c99a 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/world/LayerWorld.java @@ -31,7 +31,7 @@ import ru.windcorp.progressia.client.comms.controls.InputBasedControls; import ru.windcorp.progressia.client.graphics.Layer; import ru.windcorp.progressia.client.graphics.backend.FaceCulling; import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; -import ru.windcorp.progressia.client.graphics.input.bus.Input; +import ru.windcorp.progressia.client.graphics.input.InputEvent; import ru.windcorp.progressia.client.graphics.model.Renderable; import ru.windcorp.progressia.client.graphics.model.ShapeRenderProgram; import ru.windcorp.progressia.client.graphics.model.Shapes.PppBuilder; @@ -45,7 +45,7 @@ import ru.windcorp.progressia.common.util.Vectors; import ru.windcorp.progressia.common.world.GravityModel; import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.test.CollisionModelRenderer; -import ru.windcorp.progressia.test.TestPlayerControls; +import ru.windcorp.progressia.test.controls.TestPlayerControls; public class LayerWorld extends Layer { @@ -214,6 +214,9 @@ public class LayerWorld extends Layer { if (ClientState.getInstance().getLocalPlayer().getEntity() == entity && tmp_testControls.isFlying()) { return; } + if (entity.getId().equals("Test:NoclipCamera")) { + return; + } Vec3 gravitationalAcceleration = Vectors.grab3(); gm.getGravity(entity.getPosition(), gravitationalAcceleration); @@ -225,14 +228,9 @@ public class LayerWorld extends Layer { } @Override - protected void handleInput(Input input) { - if (input.isConsumed()) - return; - - tmp_testControls.handleInput(input); - - if (!input.isConsumed()) { - inputBasedControls.handleInput(input); + public void handleInput(InputEvent event) { + if (!event.isConsumed()) { + inputBasedControls.handleInput(event); } } diff --git a/src/main/java/ru/windcorp/progressia/client/localization/Localizer.java b/src/main/java/ru/windcorp/progressia/client/localization/Localizer.java index b61d000..ee7494d 100644 --- a/src/main/java/ru/windcorp/progressia/client/localization/Localizer.java +++ b/src/main/java/ru/windcorp/progressia/client/localization/Localizer.java @@ -72,6 +72,12 @@ public class Localizer { public synchronized String getLanguage() { return language; } + + public List getLanguages() { + List result = new ArrayList<>(langList.keySet()); + result.sort(null); + return result; + } public synchronized String getValue(String key) { if (data == null) { diff --git a/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java b/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java index 9b97e40..be85416 100644 --- a/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java +++ b/src/main/java/ru/windcorp/progressia/common/collision/colliders/Collider.java @@ -105,6 +105,9 @@ public class Collider { // For every pair of colls for (int i = 0; i < colls.size(); ++i) { Collideable a = colls.get(i); + if (a.getCollisionModel() == null) { + continue; + } tuneWorldCollisionHelper(a, tickLength, world, workspace); @@ -115,6 +118,10 @@ public class Collider { for (int j = i + 1; j < colls.size(); ++j) { Collideable b = colls.get(j); + if (b.getCollisionModel() == null) { + continue; + } + Collision collision = getCollision(a, b, tickLength, workspace); result = workspace.updateLatestCollision(result, collision); } diff --git a/src/main/java/ru/windcorp/progressia/common/state/StatefulObject.java b/src/main/java/ru/windcorp/progressia/common/state/StatefulObject.java index 2feb7e6..3b78898 100644 --- a/src/main/java/ru/windcorp/progressia/common/state/StatefulObject.java +++ b/src/main/java/ru/windcorp/progressia/common/state/StatefulObject.java @@ -235,7 +235,7 @@ public abstract class StatefulObject extends Namespaced implements Encodable { StatefulObject statefulObj = (StatefulObject) obj; - if (statefulObj.getId().equals(this.getId())) + if (!statefulObj.getId().equals(this.getId())) return false; return true; diff --git a/src/main/java/ru/windcorp/progressia/common/world/entity/EntityData.java b/src/main/java/ru/windcorp/progressia/common/world/entity/EntityData.java index bc00974..599893e 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/entity/EntityData.java +++ b/src/main/java/ru/windcorp/progressia/common/world/entity/EntityData.java @@ -379,5 +379,15 @@ public class EntityData extends StatefulObject implements Collideable, EntityGen super.read(input, context); } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + public int hashCode() { + return Long.hashCode(entityId); + } } diff --git a/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java b/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java index a9812ea..4e6cd77 100644 --- a/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java +++ b/src/main/java/ru/windcorp/progressia/test/LayerButtonTest.java @@ -33,6 +33,7 @@ import ru.windcorp.progressia.client.localization.MutableStringLocalized; import ru.windcorp.progressia.server.Player; import ru.windcorp.progressia.server.Server; import ru.windcorp.progressia.server.ServerState; +import ru.windcorp.progressia.test.controls.TestPlayerControls; public class LayerButtonTest extends MenuLayer { diff --git a/src/main/java/ru/windcorp/progressia/test/LayerDebug.java b/src/main/java/ru/windcorp/progressia/test/LayerDebug.java new file mode 100755 index 0000000..541a50e --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/test/LayerDebug.java @@ -0,0 +1,261 @@ +/* + * 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 glm.vec._3.Vec3; +import ru.windcorp.progressia.client.Client; +import ru.windcorp.progressia.client.ClientState; +import ru.windcorp.progressia.client.graphics.Colors; +import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend; +import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; +import ru.windcorp.progressia.client.graphics.font.Font; +import ru.windcorp.progressia.client.graphics.gui.DynamicLabel; +import ru.windcorp.progressia.client.graphics.gui.GUILayer; +import ru.windcorp.progressia.client.graphics.gui.Label; +import ru.windcorp.progressia.client.graphics.gui.Group; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; +import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; +import ru.windcorp.progressia.client.localization.Localizer; +import ru.windcorp.progressia.client.localization.MutableString; +import ru.windcorp.progressia.client.localization.MutableStringLocalized; +import ru.windcorp.progressia.client.world.WorldRender; +import ru.windcorp.progressia.common.Units; +import ru.windcorp.progressia.common.util.dynstr.DynamicStrings; +import ru.windcorp.progressia.server.Server; +import ru.windcorp.progressia.server.ServerState; +import ru.windcorp.progressia.test.controls.TestPlayerControls; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +public class LayerDebug extends GUILayer { + + private final List updateTriggers = new ArrayList<>(); + + public LayerDebug() { + super("LayerDebug", new LayoutAlign(0, 1, 5)); + getRoot().addChild(new Group("Displays", new LayoutVertical(5))); + + TestPlayerControls tpc = TestPlayerControls.getInstance(); + + addDynamicDisplay( + "FPSDisplay", + DynamicStrings.builder() + .addDyn(new MutableStringLocalized("LayerDebug.FPSDisplay")) + .addDyn(() -> FPS_RECORD.update(GraphicsInterface.getFPS()), 5, 1) + .addDyn(() -> GraphicsBackend.isFullscreen() ? " Fullscreen" : "") + .addDyn(() -> GraphicsBackend.isVSyncEnabled() ? " VSync" : "") + .buildSupplier() + ); + + addDynamicDisplay("TPSDisplay", LayerDebug::getTPS); + + addDynamicDisplay( + "ChunkStatsDisplay", + DynamicStrings.builder() + .addDyn(new MutableStringLocalized("LayerDebug.ChunkStatsDisplay")) + .addDyn(() -> { + if (ClientState.getInstance() == null) { + return -1; + } else { + WorldRender world = ClientState.getInstance().getWorld(); + return world.getChunks().size() - world.getPendingChunkUpdates(); + } + }, 4) + .add('/') + .addDyn(() -> { + if (ClientState.getInstance() == null) { + return -1; + } else { + return ClientState.getInstance().getWorld().getPendingChunkUpdates(); + } + }, 4) + .add('/') + .addDyn(() -> { + if (ServerState.getInstance() == null) { + return -1; + } else { + return ServerState.getInstance().getWorld().getChunks().size(); + } + }, 4) + .buildSupplier() + ); + + addDynamicDisplay("PosDisplay", LayerDebug::getPos); + + addDisplay("SelectedBlockDisplay", () -> tpc.isBlockSelected() ? ">" : " ", () -> tpc.getSelectedBlock().getId()); + addDisplay("SelectedTileDisplay", () -> tpc.isBlockSelected() ? " " : ">", () -> tpc.getSelectedTile().getId()); + addDisplay("PlacementModeHint", () -> "\u2B04"); + } + + private void addDisplay(String name, Supplier... params) { + Font font = new Font().withColor(Colors.WHITE).deriveOutlined(); + Label component = new Label(name, font, tmp_dynFormat("LayerDebug." + name, params)); + getRoot().getChild(0).addChild(component); + + for (Supplier param : params) { + if (param == null) { + continue; + } + + updateTriggers.add(new Runnable() { + + private Object displayedValue; + + @Override + public void run() { + Object newValue = param.get(); + if (!Objects.equals(newValue, displayedValue)) { + component.update(); + } + displayedValue = newValue; + } + + }); + } + } + + private void addDynamicDisplay(String name, Supplier contents) { + Font font = new Font().withColor(Colors.WHITE).deriveOutlined(); + DynamicLabel component = new DynamicLabel(name, font, contents, 128); + getRoot().getChild(0).addChild(component); + } + + @Override + protected void doRender() { + updateTriggers.forEach(Runnable::run); + super.doRender(); + } + + private static class Averager { + + private static final int DISPLAY_INERTIA = 32; + private static final double UPDATE_INTERVAL = Units.get(50.0, "ms"); + + private final double[] values = new double[DISPLAY_INERTIA]; + private int size; + private int head; + + private long lastUpdate; + + public void add(double value) { + if (size == values.length) { + values[head] = value; + head++; + if (head == values.length) + head = 0; + } else { + values[size] = value; + size++; + } + } + + public double average() { + double product = 1; + + if (size == values.length) { + for (double d : values) + product *= d; + } else { + for (int i = 0; i < size; ++i) + product *= values[i]; + } + + return Math.pow(product, 1.0 / size); + } + + public double update(double value) { + long now = (long) (GraphicsInterface.getTime() / UPDATE_INTERVAL); + if (lastUpdate != now) { + lastUpdate = now; + add(value); + } + + return average(); + } + + } + + private static final String[] CLOCK_CHARS = "\u2591\u2598\u259d\u2580\u2596\u258c\u259e\u259b\u2597\u259a\u2590\u259c\u2584\u2599\u259f\u2588" + .chars().mapToObj(c -> ((char) c) + "").toArray(String[]::new); + + private static String getTPSClockChar() { + return CLOCK_CHARS[(int) (ServerState.getInstance().getUptimeTicks() % CLOCK_CHARS.length)]; + } + + private static final Averager FPS_RECORD = new Averager(); + private static final Averager TPS_RECORD = new Averager(); + + private static final Supplier TPS_STRING = DynamicStrings.builder() + .addDyn(new MutableStringLocalized("LayerDebug.TPSDisplay")) + .addDyn(() -> TPS_RECORD.update(ServerState.getInstance().getTPS()), 5, 1) + .add(' ') + .addDyn(LayerDebug::getTPSClockChar) + .buildSupplier(); + + private static final Supplier POS_STRING = DynamicStrings.builder() + .addDyn(new MutableStringLocalized("LayerDebug.PosDisplay")) + .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().x, 7, 1) + .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().y, 7, 1) + .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().z, 7, 1) + .buildSupplier(); + + private static CharSequence getTPS() { + Server server = ServerState.getInstance(); + if (server == null) + return Localizer.getInstance().getValue("LayerDebug.TPSDisplay.NA"); + + return TPS_STRING.get(); + } + + private static CharSequence getPos() { + Client client = ClientState.getInstance(); + if (client == null) + return Localizer.getInstance().getValue("LayerDebug.PosDisplay.NA.Client"); + + Vec3 pos = client.getCamera().getLastAnchorPosition(); + if (Float.isNaN(pos.x)) { + return Localizer.getInstance().getValue("LayerDebug.PosDisplay.NA.Entity"); + } else { + return POS_STRING.get(); + } + } + + private static MutableString tmp_dynFormat(String formatKey, Supplier... suppliers) { + return new MutableStringLocalized(formatKey).apply(s -> { + Object[] args = new Object[suppliers.length]; + + for (int i = 0; i < suppliers.length; ++i) { + Supplier supplier = suppliers[i]; + + Object value = supplier != null ? supplier.get() : "null"; + if (!(value instanceof Number)) { + value = Objects.toString(value); + } + + args[i] = value; + } + + return String.format(s, args); + }); + } + +} diff --git a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java deleted file mode 100755 index 0c082fa..0000000 --- a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java +++ /dev/null @@ -1,422 +0,0 @@ -/* - * 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 glm.vec._3.Vec3; -import glm.vec._4.Vec4; -import ru.windcorp.progressia.client.Client; -import ru.windcorp.progressia.client.ClientState; -import ru.windcorp.progressia.client.graphics.Colors; -import ru.windcorp.progressia.client.graphics.backend.GraphicsBackend; -import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; -import ru.windcorp.progressia.client.graphics.font.Font; -import ru.windcorp.progressia.client.graphics.gui.DynamicLabel; -import ru.windcorp.progressia.client.graphics.gui.GUILayer; -import ru.windcorp.progressia.client.graphics.gui.Label; -import ru.windcorp.progressia.client.graphics.gui.Group; -import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; -import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; -import ru.windcorp.progressia.client.localization.Localizer; -import ru.windcorp.progressia.client.localization.MutableString; -import ru.windcorp.progressia.client.localization.MutableStringLocalized; -import ru.windcorp.progressia.client.world.WorldRender; -import ru.windcorp.progressia.common.Units; -import ru.windcorp.progressia.common.util.dynstr.DynamicStrings; -import ru.windcorp.progressia.server.Server; -import ru.windcorp.progressia.server.ServerState; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; -import java.util.function.Supplier; - -public class LayerTestGUI extends GUILayer { - - public LayerTestGUI() { - super("LayerTestGui", new LayoutAlign(0, 1, 5)); - - Group group = new Group("ControlDisplays", new LayoutVertical(5)); - - Vec4 color = Colors.WHITE; - Font font = new Font().withColor(color).deriveOutlined(); - - TestPlayerControls tpc = TestPlayerControls.getInstance(); - - group.addChild( - new Label( - "IsFlyingDisplay", - font, - tmp_dynFormat("LayerTestGUI.IsFlyingDisplay", tpc::isFlying) - ) - ); - - group.addChild( - new Label( - "IsSprintingDisplay", - font, - tmp_dynFormat("LayerTestGUI.IsSprintingDisplay", tpc::isSprinting) - ) - ); - - group.addChild( - new Label( - "CameraModeDisplay", - font, - tmp_dynFormat( - "LayerTestGUI.CameraModeDisplay", - ClientState.getInstance().getCamera()::getCurrentModeIndex - ) - ) - ); - - group.addChild( - new Label( - "LanguageDisplay", - font, - tmp_dynFormat("LayerTestGUI.LanguageDisplay", Localizer.getInstance()::getLanguage) - ) - ); - - group.addChild( - new Label( - "FullscreenDisplay", - font, - tmp_dynFormat("LayerTestGUI.IsFullscreen", GraphicsBackend::isFullscreen) - ) - ); - - group.addChild( - new Label( - "VSyncDisplay", - font, - tmp_dynFormat("LayerTestGUI.IsVSync", GraphicsBackend::isVSyncEnabled) - ) - ); - - group.addChild( - new DynamicLabel( - "FPSDisplay", - font, - DynamicStrings.builder() - .addDyn(new MutableStringLocalized("LayerTestGUI.FPSDisplay")) - .addDyn(() -> FPS_RECORD.update(GraphicsInterface.getFPS()), 5, 1) - .buildSupplier(), - 128 - ) - ); - - group.addChild( - new DynamicLabel( - "TPSDisplay", - font, - LayerTestGUI::getTPS, - 128 - ) - ); - - group.addChild( - new DynamicLabel( - "ChunkStatsDisplay", - font, - DynamicStrings.builder() - .addDyn(new MutableStringLocalized("LayerTestGUI.ChunkStatsDisplay")) - .addDyn(() -> { - if (ClientState.getInstance() == null) { - return -1; - } else { - WorldRender world = ClientState.getInstance().getWorld(); - return world.getChunks().size() - world.getPendingChunkUpdates(); - } - }, 4) - .add('/') - .addDyn(() -> { - if (ClientState.getInstance() == null) { - return -1; - } else { - return ClientState.getInstance().getWorld().getPendingChunkUpdates(); - } - }, 4) - .add('/') - .addDyn(() -> { - if (ServerState.getInstance() == null) { - return -1; - } else { - return ServerState.getInstance().getWorld().getChunks().size(); - } - }, 4) - .buildSupplier(), - 128 - ) - ); - - group.addChild( - new DynamicLabel( - "PosDisplay", - font, - LayerTestGUI::getPos, - 128 - ) - ); - - group.addChild( - new Label( - "SelectedBlockDisplay", - font, - tmp_dynFormat( - "LayerTestGUI.SelectedBlockDisplay", - () -> tpc.isBlockSelected() ? ">" : " ", - () -> tpc.getSelectedBlock().getId() - ) - ) - ); - group.addChild( - new Label( - "SelectedTileDisplay", - font, - tmp_dynFormat( - "LayerTestGUI.SelectedTileDisplay", - () -> tpc.isBlockSelected() ? " " : ">", - () -> tpc.getSelectedTile().getId() - ) - ) - ); - group.addChild( - new Label( - "PlacementModeHint", - font, - new MutableStringLocalized("LayerTestGUI.PlacementModeHint").format("\u2B04") - ) - ); - - getRoot().addChild(group); - } - - public Runnable getUpdateCallback() { - Collection