Rewrite computer input handling (again)

Hey, it lasted almost a year!

Computer input is somewhat stateful, as we need to track things like
which key(s) are currently held, and what the last mouse button/position
was. This code is currently duplicated in several places (specifically
TerminalWidget, ServerInputState and the standalone emulator).

In order to implement lectern pocket computer mouse interactions, we'd
have to duplicate this logic once again. Instead, we move this code into
a common class.

 - Replace the InputHandler interface with a simpler ComputerInput one
   (this no longer has computer actions, like reboot or terminate). This
   interface never made much sense (aside from hiding implementation
   details), as code only ever consumed a single implementation of it.

   On the client, this requires a new "ClientComputerActions" class.
   This feels a bit clunky to me, but it's simple and it works.

 - Replace ComputerEvents with a EventComputerInput class (terrible
   name, I know!), which queues events on a computer.

 - Move common input state tracking and validation into a single
   UserComputerInput class, which wraps an existing ComputerInput. This
   is used by both the terminal widget, and the server-side input state.
This commit is contained in:
Jonathan Coates
2025-12-14 16:53:18 +00:00
parent 2a35d17906
commit 28682aa468
25 changed files with 519 additions and 417 deletions
@@ -8,10 +8,10 @@ import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.core.input.UserComputerInput;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
@@ -58,7 +58,8 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
protected @Nullable TerminalWidget terminal;
protected Terminal terminalData;
protected final ComputerFamily family;
protected final InputHandler input;
protected final UserComputerInput computerInput;
protected final ClientComputerActions computerActions;
protected final int sidebarYOffset;
@@ -72,7 +73,8 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
family = container.getFamily();
displayStack = container.getDisplayStack();
uploadMaxSize = container.getUploadMaxSize();
input = new ClientInputHandler(menu);
computerInput = new UserComputerInput(new ClientComputerInput(menu), menu.getTerminal());
computerActions = new ClientComputerActions(menu);
this.sidebarYOffset = sidebarYOffset;
}
@@ -88,7 +90,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
super.init();
terminal = addRenderableWidget(createTerminal());
ComputerSidebar.addButtons(menu::isOn, input, this::addRenderableWidget, leftPos, topPos + sidebarYOffset);
ComputerSidebar.addButtons(menu::isOn, computerActions, this::addRenderableWidget, leftPos, topPos + sidebarYOffset);
setFocused(terminal);
}
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.gui;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
import net.minecraft.world.inventory.AbstractContainerMenu;
/**
* Actions that can be applied to a computer.
*
* @see ComputerActionServerMessage
*/
public final class ClientComputerActions {
private final AbstractContainerMenu menu;
public ClientComputerActions(AbstractContainerMenu menu) {
this.menu = menu;
}
public void terminate() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE));
}
public void turnOn() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
}
public void shutdown() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
}
public void reboot() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
}
@@ -5,9 +5,8 @@
package dan200.computercraft.client.gui;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.core.input.ComputerInput;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
import dan200.computercraft.shared.network.server.KeyEventServerMessage;
import dan200.computercraft.shared.network.server.MouseEventServerMessage;
import dan200.computercraft.shared.network.server.PasteEventComputerMessage;
@@ -16,37 +15,17 @@ import net.minecraft.world.inventory.AbstractContainerMenu;
import java.nio.ByteBuffer;
/**
* An {@link InputHandler} for use on the client.
* An {@link ComputerInput} for use on the client.
* <p>
* This queues events on the remote player's open {@link ComputerMenu}.
* This queues events on the player's open {@link ComputerMenu}.
*/
public final class ClientInputHandler implements InputHandler {
public final class ClientComputerInput implements ComputerInput {
private final AbstractContainerMenu menu;
public ClientInputHandler(AbstractContainerMenu menu) {
public ClientComputerInput(AbstractContainerMenu menu) {
this.menu = menu;
}
@Override
public void terminate() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE));
}
@Override
public void turnOn() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
}
@Override
public void shutdown() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
}
@Override
public void reboot() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
@Override
public void keyDown(int key, boolean repeat) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key));
@@ -33,7 +33,10 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra
@Override
protected TerminalWidget createTerminal() {
return new TerminalWidget(terminalData, input, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH + BORDER, topPos + BORDER);
return new TerminalWidget(
terminalData, computerInput, computerActions,
leftPos + AbstractComputerMenu.SIDEBAR_WIDTH + BORDER, topPos + BORDER
);
}
@Override
@@ -5,7 +5,7 @@
package dan200.computercraft.client.gui;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.input.UserComputerInput;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.KeyMapping;
@@ -29,13 +29,15 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
*/
public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen implements MenuAccess<T> {
private final T menu;
private final Terminal terminalData;
protected final UserComputerInput computerInput;
protected final ClientComputerActions computerActions;
private @Nullable TerminalWidget terminal;
public NoTermComputerScreen(T menu, Inventory player, Component title) {
super(title);
this.menu = menu;
terminalData = menu.getTerminal();
computerInput = new UserComputerInput(new ClientComputerInput(menu), menu.getTerminal());
computerActions = new ClientComputerActions(menu);
}
@Override
@@ -53,7 +55,7 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
super.init();
terminal = addWidget(new TerminalWidget(terminalData, new ClientInputHandler(menu), 0, 0));
terminal = addWidget(new TerminalWidget(menu.getTerminal(), computerInput, computerActions, 0, 0));
terminal.visible = false;
terminal.active = false;
setFocused(terminal);
@@ -40,7 +40,10 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
@Override
protected TerminalWidget createTerminal() {
return new TerminalWidget(terminalData, input, leftPos + BORDER + AbstractComputerMenu.SIDEBAR_WIDTH, topPos + BORDER);
return new TerminalWidget(
terminalData, computerInput, computerActions,
leftPos + BORDER + AbstractComputerMenu.SIDEBAR_WIDTH, topPos + BORDER
);
}
@Override
@@ -4,10 +4,10 @@
package dan200.computercraft.client.gui.widgets;
import dan200.computercraft.client.gui.ClientComputerActions;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.network.chat.Component;
@@ -34,7 +34,7 @@ public final class ComputerSidebar {
private ComputerSidebar() {
}
public static void addButtons(BooleanSupplier isOn, InputHandler input, Consumer<AbstractWidget> add, int x, int y) {
public static void addButtons(BooleanSupplier isOn, ClientComputerActions actions, Consumer<AbstractWidget> add, int x, int y) {
x += CORNERS_BORDER + 1;
y += CORNERS_BORDER + ICON_MARGIN;
@@ -46,7 +46,7 @@ public final class ComputerSidebar {
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT,
h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h),
b -> toggleComputer(isOn, input),
b -> toggleComputer(isOn, actions),
() -> isOn.getAsBoolean() ? turnOff : turnOn
));
@@ -55,7 +55,7 @@ public final class ComputerSidebar {
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT,
GuiSprites.TERMINATE::get,
b -> input.terminate(),
b -> actions.terminate(),
new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key")
@@ -71,11 +71,11 @@ public final class ComputerSidebar {
renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT);
}
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {
private static void toggleComputer(BooleanSupplier isOn, ClientComputerActions actions) {
if (isOn.getAsBoolean()) {
input.shutdown();
actions.shutdown();
} else {
input.turnOn();
actions.turnOn();
}
}
}
@@ -5,12 +5,13 @@
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.vertex.Tesselator;
import dan200.computercraft.client.gui.ClientComputerActions;
import dan200.computercraft.client.gui.ClientComputerInput;
import dan200.computercraft.client.gui.KeyConverter;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.input.UserComputerInput;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.core.InputHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractWidget;
@@ -21,8 +22,6 @@ import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.BitSet;
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
@@ -31,7 +30,7 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON
* A widget which renders a computer terminal and handles input events (keyboard, mouse, clipboard) and computer
* shortcuts (terminate/shutdown/reboot).
*
* @see dan200.computercraft.client.gui.ClientInputHandler The input handler typically used with this class.
* @see ClientComputerInput The input handler typically used with this class.
*/
public class TerminalWidget extends AbstractWidget {
private static final Component DESCRIPTION = Component.translatable("gui.computercraft.terminal");
@@ -40,7 +39,8 @@ public class TerminalWidget extends AbstractWidget {
private static final float KEY_SUPPRESS_DELAY = 0.2f;
private final Terminal terminal;
private final InputHandler computer;
private final UserComputerInput computerInput;
private final ClientComputerActions computerActions;
// The positions of the actual terminal
private final int innerX;
@@ -52,17 +52,12 @@ public class TerminalWidget extends AbstractWidget {
private float rebootTimer = -1;
private float shutdownTimer = -1;
private int lastMouseButton = -1;
private int lastMouseX = -1;
private int lastMouseY = -1;
private final BitSet keysDown = new BitSet(256);
public TerminalWidget(Terminal terminal, InputHandler computer, int x, int y) {
public TerminalWidget(Terminal terminal, UserComputerInput computerInput, ClientComputerActions computerActions, int x, int y) {
super(x, y, terminal.getWidth() * FONT_WIDTH + MARGIN * 2, terminal.getHeight() * FONT_HEIGHT + MARGIN * 2, DESCRIPTION);
this.terminal = terminal;
this.computer = computer;
this.computerInput = computerInput;
this.computerActions = computerActions;
innerX = x + MARGIN;
innerY = y + MARGIN;
@@ -72,8 +67,7 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean charTyped(char ch, int modifiers) {
var terminalChar = StringUtil.unicodeToTerminal(ch);
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
computerInput.codepointTyped(ch);
return true;
}
@@ -100,27 +94,19 @@ public class TerminalWidget extends AbstractWidget {
}
if (key >= 0 && terminateTimer < KEY_SUPPRESS_DELAY && rebootTimer < KEY_SUPPRESS_DELAY && shutdownTimer < KEY_SUPPRESS_DELAY) {
// Queue the "key" event and add to the down set
var repeat = keysDown.get(key);
keysDown.set(key);
computer.keyDown(key, repeat);
computerInput.keyDown(key);
}
return true;
}
private void paste() {
var clipboard = StringUtil.getClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (clipboard.remaining() > 0) computer.paste(clipboard);
computerInput.paste(Minecraft.getInstance().keyboardHandler.getClipboard());
}
@Override
public boolean keyReleased(int key, int scancode, int modifiers) {
// Queue the "key_up" event and remove from the down set
if (key >= 0 && keysDown.get(key)) {
keysDown.set(key, false);
computer.keyUp(key);
}
computerInput.keyUp(key);
switch (KeyConverter.physicalToActual(key, scancode)) {
case GLFW.GLFW_KEY_T -> terminateTimer = -1;
@@ -136,18 +122,10 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (!inTermRegion(mouseX, mouseY)) return false;
if (!hasMouseSupport() || button < 0 || button > 2) return false;
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
computer.mouseClick(button + 1, charX + 1, charY + 1);
lastMouseButton = button;
lastMouseX = charX;
lastMouseY = charY;
computerInput.mouseClick(button + 1, charX + 1, charY + 1);
return true;
}
@@ -155,20 +133,10 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (!inTermRegion(mouseX, mouseY)) return false;
if (!hasMouseSupport() || button < 0 || button > 2) return false;
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
if (lastMouseButton == button) {
computer.mouseUp(lastMouseButton + 1, charX + 1, charY + 1);
lastMouseButton = -1;
}
lastMouseX = charX;
lastMouseY = charY;
computerInput.mouseUp(button + 1, charX + 1, charY + 1);
return true;
}
@@ -176,36 +144,21 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double v2, double v3) {
if (!inTermRegion(mouseX, mouseY)) return false;
if (!hasMouseSupport() || button < 0 || button > 2) return false;
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
if (button == lastMouseButton && (charX != lastMouseX || charY != lastMouseY)) {
computer.mouseDrag(button + 1, charX + 1, charY + 1);
lastMouseX = charX;
lastMouseY = charY;
}
computerInput.mouseDrag(button + 1, charX + 1, charY + 1);
return true;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
if (!inTermRegion(mouseX, mouseY)) return false;
if (!hasMouseSupport() || delta == 0) return false;
if (delta == 0) return false;
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
computer.mouseScroll(delta < 0 ? 1 : -1, charX + 1, charY + 1);
lastMouseX = charX;
lastMouseY = charY;
computerInput.mouseScroll(delta < 0 ? 1 : -1, charX + 1, charY + 1);
return true;
}
@@ -214,21 +167,17 @@ public class TerminalWidget extends AbstractWidget {
return active && visible && mouseX >= innerX && mouseY >= innerY && mouseX < innerX + innerWidth && mouseY < innerY + innerHeight;
}
private boolean hasMouseSupport() {
return terminal.isColour();
}
public void update() {
if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) {
computer.terminate();
computerActions.terminate();
}
if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) {
computer.shutdown();
computerActions.shutdown();
}
if (rebootTimer >= 0 && rebootTimer < TERMINATE_TIME && (rebootTimer += 0.05f) > TERMINATE_TIME) {
computer.reboot();
computerActions.reboot();
}
}
@@ -237,18 +186,7 @@ public class TerminalWidget extends AbstractWidget {
super.setFocused(focused);
if (!focused) {
// When blurring, we should make all keys go up
for (var key = 0; key < keysDown.size(); key++) {
if (keysDown.get(key)) computer.keyUp(key);
}
keysDown.clear();
// When blurring, we should make the last mouse button go up
if (lastMouseButton >= 0) {
computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = -1;
}
computerInput.releaseInputs();
shutdownTimer = terminateTimer = rebootTimer = -1;
}
}
@@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.computer.core;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import java.nio.ByteBuffer;
/**
* Handles user-provided input, forwarding it to a computer. This describes the "shape" of both the client-and
* server-side input handlers.
*
* @see ServerInputHandler
* @see ServerComputer
*/
public interface InputHandler {
void keyDown(int key, boolean repeat);
void keyUp(int key);
void charTyped(byte chr);
void paste(ByteBuffer contents);
void mouseClick(int button, int x, int y);
void mouseUp(int button, int x, int y);
void mouseDrag(int button, int x, int y);
void mouseScroll(int direction, int x, int y);
void terminate();
void shutdown();
void turnOn();
void reboot();
}
@@ -13,8 +13,9 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.input.EventComputerInput;
import dan200.computercraft.core.input.UserComputerInput;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.impl.ApiFactories;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
@@ -37,7 +38,7 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
public class ServerComputer implements ComputerEnvironment {
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
private final int instanceID;
@@ -213,7 +214,6 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
computer.reboot();
}
@Override
public final void queueEvent(String event, @Nullable Object @Nullable [] arguments) {
computer.queueEvent(event, arguments);
}
@@ -222,6 +222,10 @@ public class ServerComputer implements ComputerEnvironment, ComputerEvents.Recei
queueEvent(event, null);
}
public final UserComputerInput createComputerInput() {
return new UserComputerInput(new EventComputerInput(computer), terminal);
}
public final int getRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0;
}
@@ -34,7 +34,7 @@ public abstract class AbstractComputerMenu extends AbstractContainerMenu impleme
private final ContainerData data;
private final @Nullable ServerComputer computer;
private final @Nullable ServerInputState<AbstractComputerMenu> input;
private final @Nullable ServerInputState input;
private final @Nullable NetworkedTerminal terminal;
@@ -51,7 +51,7 @@ public abstract class AbstractComputerMenu extends AbstractContainerMenu impleme
addDataSlots(data);
this.computer = computer;
input = computer == null ? null : new ServerInputState<>(this);
input = computer == null ? null : new ServerInputState(this, computer);
terminal = containerData == null ? null : containerData.terminal().create();
displayStack = containerData == null ? ItemStack.EMPTY : containerData.displayStack();
uploadMaxSize = containerData == null ? Config.uploadMaxSize : containerData.uploadMaxSize();
@@ -4,7 +4,7 @@
package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.core.input.ComputerInput;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.network.server.ComputerServerMessage;
@@ -14,13 +14,20 @@ import java.util.List;
import java.util.UUID;
/**
* An {@link InputHandler} which operates on the server, receiving data from the client over the network.
* An {@link ComputerInput} which operates on the server, receiving data from the client over the network.
*
* @see ServerInputState The default implementation of this interface.
* @see ComputerServerMessage Packets which consume this interface.
* @see ComputerMenu
*/
public interface ServerInputHandler extends InputHandler {
public interface ServerInputHandler {
/**
* Get a {@link ComputerInput} that handles events for this computer.
*
* @return The computer input.
*/
ComputerInput getComputerInput();
/**
* Start a file upload into this container.
*
@@ -7,15 +7,14 @@ package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.core.input.ComputerInput;
import dan200.computercraft.core.input.UserComputerInput;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.UploadResultMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -23,7 +22,6 @@ import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.UUID;
@@ -31,113 +29,31 @@ import java.util.UUID;
* The default concrete implementation of {@link ServerInputHandler}.
* <p>
* This keeps track of the current key and mouse state, and releases them when the container is closed.
*
* @param <T> The type of container this server input belongs to.
*/
public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> implements ServerInputHandler {
public class ServerInputState implements ServerInputHandler {
private static final Logger LOG = LoggerFactory.getLogger(ServerInputState.class);
private final T owner;
private final IntSet keysDown = new IntOpenHashSet(4);
private int lastMouseX;
private int lastMouseY;
private int lastMouseDown = -1;
private final AbstractContainerMenu owner;
private final ServerComputer computer;
private final UserComputerInput input;
private @Nullable UUID toUploadId;
private @Nullable List<FileUpload> toUpload;
public ServerInputState(T owner) {
public ServerInputState(AbstractContainerMenu owner, ServerComputer computer) {
this.owner = owner;
this.computer = computer;
this.input = computer.createComputerInput();
}
@Override
public void keyDown(int key, boolean repeat) {
keysDown.add(key);
ComputerEvents.keyDown(owner.getComputer(), key, repeat);
public ComputerInput getComputerInput() {
return input;
}
@Override
public void keyUp(int key) {
keysDown.remove(key);
ComputerEvents.keyUp(owner.getComputer(), key);
}
@Override
public void charTyped(byte chr) {
if (StringUtil.isTypableChar(chr)) ComputerEvents.charTyped(owner.getComputer(), chr);
}
@Override
public void paste(ByteBuffer contents) {
if (contents.remaining() > 0 && isValidClipboard(contents)) ComputerEvents.paste(owner.getComputer(), contents);
}
private static boolean isValidClipboard(ByteBuffer buffer) {
for (int i = buffer.position(), max = buffer.limit(); i < max; i++) {
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
}
return true;
}
@Override
public void mouseClick(int button, int x, int y) {
lastMouseX = x;
lastMouseY = y;
lastMouseDown = button;
ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
}
@Override
public void mouseUp(int button, int x, int y) {
lastMouseX = x;
lastMouseY = y;
lastMouseDown = -1;
ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
}
@Override
public void mouseDrag(int button, int x, int y) {
lastMouseX = x;
lastMouseY = y;
lastMouseDown = button;
ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
}
@Override
public void mouseScroll(int direction, int x, int y) {
lastMouseX = x;
lastMouseY = y;
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
}
@Override
public void terminate() {
owner.getComputer().queueEvent("terminate");
}
@Override
public void shutdown() {
owner.getComputer().shutdown();
}
@Override
public void turnOn() {
owner.getComputer().turnOn();
}
@Override
public void reboot() {
owner.getComputer().reboot();
}
@Override
public void startUpload(UUID uuid, List<FileUpload> files) {
toUploadId = uuid;
public void startUpload(UUID uploadId, List<FileUpload> files) {
toUploadId = uploadId;
toUpload = files;
}
@@ -162,7 +78,6 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
}
private UploadResultMessage finishUpload(ServerPlayer player) {
var computer = owner.getComputer();
if (toUpload == null) {
return UploadResultMessage.error(owner, UploadResult.COMPUTER_OFF_MSG);
}
@@ -187,13 +102,6 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
}
public void close() {
var computer = owner.getComputer();
var keys = keysDown.iterator();
while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt());
if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY);
keysDown.clear();
lastMouseDown = -1;
input.releaseInputs();
}
}
@@ -33,10 +33,10 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
switch (action) {
case TERMINATE -> container.getInput().terminate();
case TURN_ON -> container.getInput().turnOn();
case REBOOT -> container.getInput().reboot();
case SHUTDOWN -> container.getInput().shutdown();
case TERMINATE -> container.getComputer().queueEvent("terminate");
case TURN_ON -> container.getComputer().turnOn();
case REBOOT -> container.getComputer().reboot();
case SHUTDOWN -> container.getComputer().shutdown();
}
}
@@ -36,7 +36,7 @@ public class KeyEventServerMessage extends ComputerServerMessage {
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
var input = container.getInput();
var input = container.getInput().getComputerInput();
switch (type) {
case UP -> input.keyUp(key);
case DOWN -> input.keyDown(key, false);
@@ -44,7 +44,7 @@ public class MouseEventServerMessage extends ComputerServerMessage {
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
var input = container.getInput();
var input = container.getInput().getComputerInput();
switch (type) {
case CLICK -> input.mouseClick(arg, x, y);
case DRAG -> input.mouseDrag(arg, x, y);
@@ -4,10 +4,10 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.core.input.ComputerInput;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import io.netty.handler.codec.DecoderException;
@@ -19,7 +19,7 @@ import java.nio.ByteBuffer;
/**
* Paste a string on a {@link ServerComputer}.
*
* @see ServerInputHandler#paste(ByteBuffer)
* @see ComputerInput#paste(ByteBuffer)
*/
public class PasteEventComputerMessage extends ComputerServerMessage {
private final ByteBuffer text;
@@ -51,7 +51,7 @@ public class PasteEventComputerMessage extends ComputerServerMessage {
@Override
protected void handle(ServerNetworkContext context, ComputerMenu container) {
container.getInput().paste(text);
container.getInput().getComputerInput().paste(text);
}
@Override
@@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicLong;
* <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
* </ul>
*/
public class Computer implements ComputerEvents.Receiver {
public class Computer {
private static final int START_DELAY = 50;
// Various properties of the computer
@@ -114,7 +114,6 @@ public class Computer implements ComputerEvents.Receiver {
executor.queueStop(false, true);
}
@Override
public void queueEvent(String event, @Nullable Object @Nullable [] args) {
executor.queueEvent(event, args);
}
@@ -1,72 +0,0 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.computer;
import dan200.computercraft.core.util.StringUtil;
import org.jspecify.annotations.Nullable;
import java.nio.ByteBuffer;
/**
* Built-in events that can be queued on a computer.
*/
public final class ComputerEvents {
private ComputerEvents() {
}
public static void keyDown(Receiver receiver, int key, boolean repeat) {
receiver.queueEvent("key", new Object[]{ key, repeat });
}
public static void keyUp(Receiver receiver, int key) {
receiver.queueEvent("key_up", new Object[]{ key });
}
/**
* Type a character on the computer.
*
* @param receiver The computer to queue the event on.
* @param chr The character to type.
* @see StringUtil#isTypableChar(byte)
*/
public static void charTyped(Receiver receiver, byte chr) {
receiver.queueEvent("char", new Object[]{ new byte[]{ chr } });
}
/**
* Paste a string.
*
* @param receiver The computer to queue the event on.
* @param contents The string to paste.
* @see StringUtil#getClipboardString(String)
*/
public static void paste(Receiver receiver, ByteBuffer contents) {
receiver.queueEvent("paste", new Object[]{ contents });
}
public static void mouseClick(Receiver receiver, int button, int x, int y) {
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
}
public static void mouseUp(Receiver receiver, int button, int x, int y) {
receiver.queueEvent("mouse_up", new Object[]{ button, x, y });
}
public static void mouseDrag(Receiver receiver, int button, int x, int y) {
receiver.queueEvent("mouse_drag", new Object[]{ button, x, y });
}
public static void mouseScroll(Receiver receiver, int direction, int x, int y) {
receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y });
}
/**
* An object that can receive computer events.
*/
@FunctionalInterface
public interface Receiver {
void queueEvent(String event, @Nullable Object @Nullable [] arguments);
}
}
@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.input;
import dan200.computercraft.core.util.StringUtil;
import java.nio.ByteBuffer;
/**
* Input events that can be performed on a computer.
*
* @see EventComputerInput
* @see UserComputerInput
*/
public interface ComputerInput {
/**
* Queue a {@code key} event.
*
* @param key The key that was pressed.
* @param repeat Whether this is a repeat input.
*/
void keyDown(int key, boolean repeat);
/**
* Queue a {@code key_up} event.
*
* @param key The key that was released.
*/
void keyUp(int key);
/**
* Type a character on the computer.
*
* @param chr The character to type.
* @see StringUtil#isTypableChar(byte)
*/
void charTyped(byte chr);
/**
* Paste a string.
*
* @param contents The string to paste.
* @see StringUtil#getClipboardString(String)
*/
void paste(ByteBuffer contents);
/**
* Queue a {@code mouse_click} event.
*
* @param button The mouse button that was pressed, between 1 and 3 (inclusive).
* @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive).
* @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive).
*/
void mouseClick(int button, int x, int y);
/**
* Queue a {@code mouse_up} event.
*
* @param button The mouse button that was released, between 1 and 3 (inclusive).
* @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive).
* @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive).
*/
void mouseUp(int button, int x, int y);
/**
* Queue a {@code mouse_drag} event.
*
* @param button The mouse button that is being pressed, between 1 and 3 (inclusive).
* @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive).
* @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive).
*/
void mouseDrag(int button, int x, int y);
/**
* Queue a {@code mouse_scroll} event.
*
* @param direction The direction of the scroll, where negative values are up and positive ones are down.
* @param x The x coordinate of the mouse, between 1 and the terminal width (inclusive).
* @param y The y coordinate of the mouse, between 1 and the terminal height (inclusive).
*/
void mouseScroll(int direction, int x, int y);
}
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.input;
import dan200.computercraft.core.computer.Computer;
import org.jspecify.annotations.Nullable;
import java.nio.ByteBuffer;
/**
* A {@link ComputerInput} that queues events on the computer.
*/
public final class EventComputerInput implements ComputerInput {
private final QueueEvent receiver;
public EventComputerInput(QueueEvent receiver) {
this.receiver = receiver;
}
public EventComputerInput(Computer computer) {
this(computer::queueEvent);
}
@Override
public void keyDown(int key, boolean repeat) {
receiver.queueEvent("key", new Object[]{ key, repeat });
}
@Override
public void keyUp(int key) {
receiver.queueEvent("key_up", new Object[]{ key });
}
@Override
public void charTyped(byte chr) {
receiver.queueEvent("char", new Object[]{ new byte[]{ chr } });
}
@Override
public void paste(ByteBuffer contents) {
receiver.queueEvent("paste", new Object[]{ contents });
}
@Override
public void mouseClick(int button, int x, int y) {
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
}
@Override
public void mouseUp(int button, int x, int y) {
receiver.queueEvent("mouse_up", new Object[]{ button, x, y });
}
@Override
public void mouseDrag(int button, int x, int y) {
receiver.queueEvent("mouse_drag", new Object[]{ button, x, y });
}
@Override
public void mouseScroll(int direction, int x, int y) {
receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y });
}
/**
* A function to queue events.
*/
@FunctionalInterface
public interface QueueEvent {
void queueEvent(String event, @Nullable Object @Nullable [] arguments);
}
}
@@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.input;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import java.nio.ByteBuffer;
/**
* A {@link ComputerInput} that wraps an existing {@link ComputerInput}. This both validates any user inputs (e.g.
* ensuring mouse presses only happen on an advanced terminal), and supports {@linkplain #releaseInputs()
* releasing any held inputs} (e.g. for when the computer loses focus).
*/
public final class UserComputerInput implements ComputerInput {
private final ComputerInput delegate;
private final boolean mouseSupport;
private final int termWidth;
private final int termHeight;
private final IntSet keysDown = new IntOpenHashSet(4);
private int lastMouseX;
private int lastMouseY;
private int lastMouseDown = -1;
public UserComputerInput(ComputerInput delegate, boolean mouseSupport, int termWidth, int termHeight) {
this.delegate = delegate;
this.mouseSupport = mouseSupport;
this.termWidth = termWidth;
this.termHeight = termHeight;
}
public UserComputerInput(ComputerInput delegate, Terminal terminal) {
this(delegate, terminal.isColour(), terminal.getWidth(), terminal.getHeight());
}
@Override
public void keyDown(int key, boolean repeat) {
if (key < 0) return;
keysDown.add(key);
delegate.keyDown(key, repeat);
}
/**
* Queue a {@code key} event on the computer. This behaves the same as {@link #keyDown(int, boolean)}, but infers
* the {@code "repeat} state from the currently held keys.
*
* @param key The key to press.
*/
public void keyDown(int key) {
keyDown(key, keysDown.contains(key));
}
@Override
public void keyUp(int key) {
if (key < 0) return;
keysDown.remove(key);
delegate.keyUp(key);
}
@Override
public void charTyped(byte chr) {
delegate.charTyped(chr);
}
public void codepointTyped(int codepoint) {
var terminalChar = StringUtil.unicodeToTerminal(codepoint);
if (StringUtil.isTypableChar(terminalChar)) charTyped((byte) terminalChar);
}
private static boolean isValidClipboard(ByteBuffer buffer) {
for (int i = buffer.position(), max = buffer.limit(); i < max; i++) {
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
}
return true;
}
@Override
public void paste(ByteBuffer contents) {
if (contents.remaining() > 0 && isValidClipboard(contents)) delegate.paste(contents);
}
/**
* Paste a string.
*
* @param contents The string to paste.
*/
public void paste(String contents) {
paste(StringUtil.getClipboardString(contents));
}
@Override
public void mouseClick(int button, int x, int y) {
if (!mouseSupport || button < 1 || button > 3) return;
var clampedX = lastMouseX = Math.min(Math.max(x, 1), termWidth);
var clampedY = lastMouseY = Math.min(Math.max(y, 1), termHeight);
delegate.mouseClick(button, clampedX, clampedY);
lastMouseDown = button;
}
/**
* Queue a {@code mouse_click} event on the computer. This behaves the same as {@link #mouseClick(int, int, int)},
* but infers the mouse position from the last mouse position.
*
* @param button The mouse button pressed, between 1 and 3.
*/
public void mouseClick(int button) {
mouseClick(button, lastMouseX, lastMouseY);
}
@Override
public void mouseUp(int button, int x, int y) {
if (!mouseSupport || button < 1 || button > 3) return;
var clampedX = lastMouseX = Math.min(Math.max(x, 1), termWidth);
var clampedY = lastMouseY = Math.min(Math.max(y, 1), termHeight);
if (lastMouseDown == button) {
delegate.mouseUp(button, clampedX, clampedY);
lastMouseDown = -1;
}
}
/**
* Queue a {@code mouse_scroll} event on the computer. This behaves the same as {@link #mouseUp(int, int, int)},
* but infers the mouse position from the last mouse position.
*
* @param button The mouse button released, between 1 and 3.
*/
public void mouseUp(int button) {
mouseUp(button, lastMouseX, lastMouseY);
}
@Override
public void mouseDrag(int button, int x, int y) {
if (!mouseSupport || button < 1 || button > 3) return;
var clampedX = Math.min(Math.max(x, 1), termWidth);
var clampedY = Math.min(Math.max(y, 1), termHeight);
if (button == lastMouseDown && (clampedX != lastMouseX || clampedY != lastMouseY)) {
delegate.mouseDrag(button, clampedX, clampedY);
lastMouseX = clampedX;
lastMouseY = clampedY;
}
}
/**
* Update the mouse position, and optionally queue a {@code mouse_drag} event on the computer.
* <p>
* This is similar to {@link #mouseDrag(int, int, int)}, but when the currently clicked button is not available.
*
* @param x The X position of the mouse, between 1 and the terminal width.
* @param y The Y position of the mouse, between 1 and the terminal width.
*/
public void mouseMove(int x, int y) {
if (!mouseSupport) return;
var clampedX = Math.min(Math.max(x, 1), termWidth);
var clampedY = Math.min(Math.max(y, 1), termHeight);
if (lastMouseDown != -1 && (clampedX != lastMouseX || clampedY != lastMouseY)) {
delegate.mouseDrag(lastMouseDown, clampedX, clampedY);
}
lastMouseX = clampedX;
lastMouseY = clampedY;
}
@Override
public void mouseScroll(int direction, int x, int y) {
if (!mouseSupport || direction == 0) return;
var clampedX = lastMouseX = Math.min(Math.max(x, 1), termWidth);
var clampedY = lastMouseY = Math.min(Math.max(y, 1), termHeight);
delegate.mouseScroll(direction, clampedX, clampedY);
}
/**
* Queue a {@code mouse_scroll} event on the computer. This behaves the same as {@link #mouseScroll(int, int, int)},
* but infers the mouse position from the last mouse position.
*
* @param direction The direction of the scroll.
*/
public void mouseScroll(int direction) {
mouseScroll(direction, lastMouseX, lastMouseY);
}
/**
* Release all currently held inputs, such as held keys and pressed mouse buttons.
*/
public void releaseInputs() {
// Release all keys
var keys = keysDown.iterator();
while (keys.hasNext()) delegate.keyUp(keys.nextInt());
keysDown.clear();
// Release last held mouse button.
if (lastMouseDown != -1) {
delegate.mouseUp(lastMouseDown, lastMouseX, lastMouseY);
lastMouseDown = -1;
}
}
}
@@ -4,7 +4,7 @@
package dan200.computercraft.core.util;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.input.ComputerInput;
import java.nio.ByteBuffer;
@@ -71,8 +71,8 @@ public final class StringUtil {
}
/**
* Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte)
* "char" event}.
* Check if a character is capable of being input and passed to a {@linkplain ComputerInput#charTyped(byte) "char"
* event}.
*
* @param chr The character to check.
* @return Whether this character can be typed.
@@ -82,8 +82,8 @@ public final class StringUtil {
}
/**
* Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte)
* "char" event}.
* Check if a character is capable of being input and passed to a {@linkplain ComputerInput#charTyped(byte) "char"
* * event}.
*
* @param chr The character to check.
* @return Whether this character can be typed.
@@ -8,8 +8,8 @@ import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.core.input.EventComputerInput;
import dan200.computercraft.core.input.UserComputerInput;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWDropCallback;
import org.lwjgl.glfw.GLFWKeyCallbackI;
@@ -21,7 +21,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
/**
@@ -35,23 +34,19 @@ public class InputState {
private static final float KEY_SUPPRESS_DELAY = 0.2f;
private final Computer computer;
private final BitSet keysDown = new BitSet(256);
private final UserComputerInput input;
private float terminateTimer = -1;
private float rebootTimer = -1;
private float shutdownTimer = -1;
private int lastMouseButton = -1;
private int lastMouseX = -1;
private int lastMouseY = -1;
public InputState(Computer computer) {
this.computer = computer;
this.input = new UserComputerInput(new EventComputerInput(computer), computer.getEnvironment().getTerminal());
}
public void onCharEvent(int codepoint) {
var terminalChar = StringUtil.unicodeToTerminal(codepoint);
if (StringUtil.isTypableChar(terminalChar)) ComputerEvents.charTyped(computer, (byte) terminalChar);
input.codepointTyped(codepoint);
}
public void onKeyEvent(long window, int key, int action, int modifiers) {
@@ -66,10 +61,7 @@ public class InputState {
if (key == GLFW.GLFW_KEY_V && modifiers == GLFW.GLFW_MOD_CONTROL) {
var string = GLFW.glfwGetClipboardString(window);
if (string != null) {
var clipboard = StringUtil.getClipboardString(string);
if (clipboard.remaining() > 0) ComputerEvents.paste(computer, clipboard);
}
if (string != null) input.paste(string);
return;
}
@@ -88,19 +80,12 @@ public class InputState {
}
if (key >= 0 && terminateTimer < KEY_SUPPRESS_DELAY && rebootTimer < KEY_SUPPRESS_DELAY && shutdownTimer < KEY_SUPPRESS_DELAY) {
// Queue the "key" event and add to the down set
var repeat = keysDown.get(key);
keysDown.set(key);
ComputerEvents.keyDown(computer, key, repeat);
input.keyDown(key);
}
}
private void keyReleased(int key) {
// Queue the "key_up" event and remove from the down set
if (key >= 0 && keysDown.get(key)) {
keysDown.set(key, false);
ComputerEvents.keyUp(computer, key);
}
input.keyUp(key);
switch (key) {
case GLFW.GLFW_KEY_T -> terminateTimer = -1;
@@ -113,33 +98,17 @@ public class InputState {
public void onMouseClick(int button, int action) {
switch (action) {
case GLFW.GLFW_PRESS -> {
ComputerEvents.mouseClick(computer, button + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = button;
}
case GLFW.GLFW_RELEASE -> {
if (button == lastMouseButton) {
ComputerEvents.mouseUp(computer, button + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = -1;
}
}
case GLFW.GLFW_PRESS -> input.mouseClick(button + 1);
case GLFW.GLFW_RELEASE -> input.mouseUp(button + 1);
}
}
public void onMouseMove(int mouseX, int mouseY) {
if (mouseX == lastMouseX && mouseY == lastMouseY) return;
lastMouseX = mouseX;
lastMouseY = mouseY;
if (lastMouseButton != -1) {
ComputerEvents.mouseDrag(computer, lastMouseButton + 1, mouseX + 1, mouseY + 1);
}
input.mouseMove(mouseX + 1, mouseY + 1);
}
public void onMouseScroll(double yOffset) {
if (yOffset != 0) {
ComputerEvents.mouseScroll(computer, yOffset < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1);
}
if (yOffset != 0) input.mouseScroll(yOffset < 0 ? 1 : -1);
}
public void onFileDrop(int count, long names) {
@@ -313,8 +313,6 @@ public class Main {
glfwSetCursorPosCallback(window, (w, x, y) -> {
var charX = (int) (((x / SCALE) - MARGIN) / PIXEL_WIDTH);
var charY = (int) (((y / SCALE) - MARGIN) / PIXEL_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
inputState.onMouseMove(charX, charY);
});
glfwSetScrollCallback(window, (w, xOffset, yOffset) -> inputState.onMouseScroll(yOffset));