Bläddra i källkod

Merge pull request #7 from architectury/feature/gui-input

Add ClientScreenInputEvent
shedaniel 4 år sedan
förälder
incheckning
b8b419c6da

+ 74 - 0
common/src/main/java/me/shedaniel/architectury/event/events/client/ClientScreenInputEvent.java

@@ -0,0 +1,74 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.event.events.client;
+
+import me.shedaniel.architectury.event.Event;
+import me.shedaniel.architectury.event.EventFactory;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.world.InteractionResult;
+
+@Environment(EnvType.CLIENT)
+public interface ClientScreenInputEvent {
+    Event<MouseScrolled> MOUSE_SCROLLED_PRE = EventFactory.createInteractionResult(MouseScrolled.class);
+    Event<MouseScrolled> MOUSE_SCROLLED_POST = EventFactory.createInteractionResult(MouseScrolled.class);
+    Event<MouseClicked> MOUSE_CLICKED_PRE = EventFactory.createInteractionResult(MouseClicked.class);
+    Event<MouseClicked> MOUSE_CLICKED_POST = EventFactory.createInteractionResult(MouseClicked.class);
+    Event<MouseReleased> MOUSE_RELEASED_PRE = EventFactory.createInteractionResult(MouseReleased.class);
+    Event<MouseReleased> MOUSE_RELEASED_POST = EventFactory.createInteractionResult(MouseReleased.class);
+    Event<MouseDragged> MOUSE_DRAGGED_PRE = EventFactory.createInteractionResult(MouseDragged.class);
+    Event<MouseDragged> MOUSE_DRAGGED_POST = EventFactory.createInteractionResult(MouseDragged.class);
+    Event<KeyTyped> CHAR_TYPED_PRE = EventFactory.createInteractionResult(KeyTyped.class);
+    Event<KeyTyped> CHAR_TYPED_POST = EventFactory.createInteractionResult(KeyTyped.class);
+    Event<KeyPressed> KEY_PRESSED_PRE = EventFactory.createInteractionResult(KeyPressed.class);
+    Event<KeyPressed> KEY_PRESSED_POST = EventFactory.createInteractionResult(KeyPressed.class);
+    Event<KeyReleased> KEY_RELEASED_PRE = EventFactory.createInteractionResult(KeyReleased.class);
+    Event<KeyReleased> KEY_RELEASED_POST = EventFactory.createInteractionResult(KeyReleased.class);
+    
+    interface KeyPressed {
+        InteractionResult keyPressed(Minecraft client, Screen screen, int keyCode, int scanCode, int modifiers);
+    }
+    
+    interface KeyReleased {
+        InteractionResult keyReleased(Minecraft client, Screen screen, int keyCode, int scanCode, int modifiers);
+    }
+    
+    interface KeyTyped {
+        InteractionResult charTyped(Minecraft client, Screen screen, char character, int keyCode);
+    }
+    
+    interface MouseScrolled {
+        InteractionResult mouseScrolled(Minecraft client, Screen screen, double mouseX, double mouseY, double amount);
+    }
+    
+    interface MouseReleased {
+        InteractionResult mouseReleased(Minecraft client, Screen screen, double mouseX, double mouseY, int button);
+    }
+    
+    interface MouseDragged {
+        InteractionResult mouseDragged(Minecraft client, Screen screen, double mouseX1, double mouseY1, int button, double mouseX2, double mouseY2);
+    }
+    
+    interface MouseClicked {
+        InteractionResult mouseClicked(Minecraft client, Screen screen, double mouseX, double mouseY, int button);
+    }
+}

+ 19 - 0
common/src/test/java/me/shedaniel/architectury/test/ConsoleMessageSink.java

@@ -1,3 +1,22 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
 package me.shedaniel.architectury.test;
 
 import org.apache.logging.log4j.LogManager;

+ 19 - 0
common/src/test/java/me/shedaniel/architectury/test/MessageSink.java

@@ -1,3 +1,22 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
 package me.shedaniel.architectury.test;
 
 public interface MessageSink {

+ 49 - 0
common/src/test/java/me/shedaniel/architectury/test/TestMod.java

@@ -1,7 +1,28 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
 package me.shedaniel.architectury.test;
 
+import com.mojang.blaze3d.platform.InputConstants;
 import me.shedaniel.architectury.event.events.*;
 import me.shedaniel.architectury.event.events.client.ClientChatEvent;
+import me.shedaniel.architectury.event.events.client.ClientScreenInputEvent;
 import me.shedaniel.architectury.event.events.client.ClientLifecycleEvent;
 import me.shedaniel.architectury.event.events.client.ClientPlayerEvent;
 import me.shedaniel.architectury.hooks.ExplosionHooks;
@@ -203,6 +224,34 @@ public class TestMod {
         TextureStitchEvent.POST.register(atlas -> {
             SINK.accept("Client texture stitched: " + atlas.location());
         });
+        ClientScreenInputEvent.MOUSE_SCROLLED_PRE.register((client, screen, mouseX, mouseY, amount) -> {
+            SINK.accept("Screen Mouse amount: %.2f distance", amount);
+            return InteractionResult.PASS;
+        });
+        ClientScreenInputEvent.MOUSE_CLICKED_PRE.register((client, screen, mouseX, mouseY, button) -> {
+            SINK.accept("Screen Mouse clicked: " + button);
+            return InteractionResult.PASS;
+        });
+        ClientScreenInputEvent.MOUSE_RELEASED_PRE.register((client, screen, mouseX, mouseY, button) -> {
+            SINK.accept("Screen Mouse released: " + button);
+            return InteractionResult.PASS;
+        });
+        ClientScreenInputEvent.MOUSE_DRAGGED_PRE.register((client, screen, mouseX1, mouseY1, button, mouseX2, mouseY2) -> {
+            SINK.accept("Screen Mouse dragged: %d (%d,%d) by (%d,%d)", button, (int) mouseX1, (int) mouseY1, (int) mouseX2, (int) mouseY2);
+            return InteractionResult.PASS;
+        });
+        ClientScreenInputEvent.CHAR_TYPED_PRE.register((client, screen, character, keyCode) -> {
+            SINK.accept("Screen Char typed: " + character);
+            return InteractionResult.PASS;
+        });
+        ClientScreenInputEvent.KEY_PRESSED_PRE.register((client, screen, keyCode, scanCode, modifiers) -> {
+            SINK.accept("Screen Key pressed: " + InputConstants.getKey(keyCode, scanCode).getDisplayName().getString());
+            return InteractionResult.PASS;
+        });
+        ClientScreenInputEvent.KEY_RELEASED_PRE.register((client, screen, keyCode, scanCode, modifiers) -> {
+            SINK.accept("Screen Key released: " + InputConstants.getKey(keyCode, scanCode).getDisplayName().getString());
+            return InteractionResult.PASS;
+        });
     }
     
     private static String toSimpleName(Object o) {

+ 25 - 4
common/src/test/java/me/shedaniel/architectury/test/client/ClientOverlayMessageSink.java

@@ -1,3 +1,22 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
 package me.shedaniel.architectury.test.client;
 
 import com.google.common.collect.Lists;
@@ -55,10 +74,12 @@ public class ClientOverlayMessageSink extends ConsoleMessageSink {
                 if (timeExisted >= 5000) {
                     messageIterator.remove();
                 } else {
-                    int textWidth = minecraft.font.width(message.text);
-                    int alpha = (int) Mth.clamp((5000 - timeExisted) / 5000f * 400f + 8, 0, 255);
-                    GuiComponent.fill(matrices, 0, y - 1, 2 + textWidth + 1, y + lineHeight - 1, 0x505050 + ((alpha * 144 / 255) << 24));
-                    minecraft.font.draw(matrices, message.text, 1, y, 0xE0E0E0 + (alpha << 24));
+                    if (y - 1 < minecraft.getWindow().getGuiScaledHeight()) {
+                        int textWidth = minecraft.font.width(message.text);
+                        int alpha = (int) Mth.clamp((5000 - timeExisted) / 5000f * 400f + 8, 0, 255);
+                        GuiComponent.fill(matrices, 0, y - 1, 2 + textWidth + 1, y + lineHeight - 1, 0x505050 + ((alpha * 144 / 255) << 24));
+                        minecraft.font.draw(matrices, message.text, 1, y, 0xE0E0E0 + (alpha << 24));
+                    }
                     y += lineHeight;
                 }
             }

+ 58 - 0
fabric/src/main/java/me/shedaniel/architectury/impl/fabric/ScreenInputDelegate.java

@@ -0,0 +1,58 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.impl.fabric;
+
+import me.shedaniel.architectury.event.events.client.ClientScreenInputEvent;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.TextComponent;
+import net.minecraft.world.InteractionResult;
+
+public interface ScreenInputDelegate {
+    GuiEventListener architectury_delegateInputs();
+    
+    class DelegateScreen extends Screen {
+        private Screen parent;
+        
+        public DelegateScreen(Screen parent) {
+            super(TextComponent.EMPTY);
+            this.parent = parent;
+        }
+        
+        @Override
+        public boolean mouseDragged(double d, double e, int i, double f, double g) {
+            if (ClientScreenInputEvent.MOUSE_DRAGGED_PRE.invoker().mouseDragged(Minecraft.getInstance(), parent, d, e, i, f, g) != InteractionResult.PASS)
+                return true;
+            if (parent.mouseDragged(d, e, i, f, g))
+                return true;
+            return ClientScreenInputEvent.MOUSE_DRAGGED_PRE.invoker().mouseDragged(Minecraft.getInstance(), parent, d, e, i, f, g) != InteractionResult.PASS;
+        }
+        
+        @Override
+        public boolean charTyped(char c, int i) {
+            if (ClientScreenInputEvent.CHAR_TYPED_PRE.invoker().charTyped(Minecraft.getInstance(), parent, c, i) != InteractionResult.PASS)
+                return true;
+            if (parent.charTyped(c, i))
+                return true;
+            return ClientScreenInputEvent.CHAR_TYPED_POST.invoker().charTyped(Minecraft.getInstance(), parent, c, i) != InteractionResult.PASS;
+        }
+    }
+}

+ 100 - 0
fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/client/MixinKeyboardHandler.java

@@ -0,0 +1,100 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.mixin.fabric.client;
+
+import me.shedaniel.architectury.event.events.client.ClientScreenInputEvent;
+import me.shedaniel.architectury.impl.fabric.ScreenInputDelegate;
+import net.minecraft.client.KeyboardHandler;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.components.events.ContainerEventHandler;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.world.InteractionResult;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+@Mixin(KeyboardHandler.class)
+public class MixinKeyboardHandler {
+    @Shadow
+    @Final
+    private Minecraft minecraft;
+    
+    @Shadow
+    private boolean sendRepeatsToGui;
+    
+    @SuppressWarnings("UnresolvedMixinReference")
+    @ModifyVariable(method = {"method_1458", "lambda$charTyped$5"}, at = @At("HEAD"), ordinal = 0, argsOnly = true)
+    private static GuiEventListener wrapCharTypedFirst(GuiEventListener screen) {
+        if (screen instanceof ScreenInputDelegate) {
+            return ((ScreenInputDelegate) screen).architectury_delegateInputs();
+        }
+        return screen;
+    }
+    
+    @SuppressWarnings("UnresolvedMixinReference")
+    @ModifyVariable(method = {"method_1473", "lambda$charTyped$6"}, at = @At("HEAD"), ordinal = 0, argsOnly = true)
+    private static GuiEventListener wrapCharTypedSecond(GuiEventListener screen) {
+        if (screen instanceof ScreenInputDelegate) {
+            return ((ScreenInputDelegate) screen).architectury_delegateInputs();
+        }
+        return screen;
+    }
+    
+    @Inject(method = "keyPress", at = @At(value = "INVOKE",
+                                          target = "Lnet/minecraft/client/gui/screens/Screen;wrapScreenError(Ljava/lang/Runnable;Ljava/lang/String;Ljava/lang/String;)V",
+                                          ordinal = 0), cancellable = true)
+    public void onKey(long long_1, int int_1, int int_2, int int_3, int int_4, CallbackInfo info) {
+        if (!info.isCancelled()) {
+            if (int_3 != 1 && (int_3 != 2 || !this.sendRepeatsToGui)) {
+                if (int_3 == 0) {
+                    InteractionResult result = ClientScreenInputEvent.KEY_RELEASED_PRE.invoker().keyReleased(minecraft, minecraft.screen, int_1, int_2, int_4);
+                    if (result != InteractionResult.PASS)
+                        info.cancel();
+                }
+            } else {
+                InteractionResult result = ClientScreenInputEvent.KEY_PRESSED_PRE.invoker().keyPressed(minecraft, minecraft.screen, int_1, int_2, int_4);
+                if (result != InteractionResult.PASS)
+                    info.cancel();
+            }
+        }
+    }
+    
+    @Inject(method = "keyPress", at = @At(value = "INVOKE",
+                                          target = "Lnet/minecraft/client/gui/screens/Screen;wrapScreenError(Ljava/lang/Runnable;Ljava/lang/String;Ljava/lang/String;)V",
+                                          ordinal = 0, shift = At.Shift.AFTER), locals = LocalCapture.CAPTURE_FAILHARD,
+            cancellable = true)
+    public void onKeyAfter(long long_1, int int_1, int int_2, int int_3, int int_4, CallbackInfo info, ContainerEventHandler containerEventHandler, boolean bls[]) {
+        if (!info.isCancelled() && !bls[0]) {
+            InteractionResult result;
+            if (int_3 != 1 && (int_3 != 2 || !this.sendRepeatsToGui)) {
+                result = ClientScreenInputEvent.KEY_RELEASED_POST.invoker().keyReleased(minecraft, minecraft.screen, int_1, int_2, int_4);
+            } else {
+                result = ClientScreenInputEvent.KEY_PRESSED_POST.invoker().keyPressed(minecraft, minecraft.screen, int_1, int_2, int_4);
+            }
+            if (result != InteractionResult.PASS)
+                info.cancel();
+        }
+    }
+}

+ 126 - 0
fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/client/MixinMouseHandler.java

@@ -0,0 +1,126 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021 shedaniel
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package me.shedaniel.architectury.mixin.fabric.client;
+
+import me.shedaniel.architectury.event.events.client.ClientScreenInputEvent;
+import me.shedaniel.architectury.impl.fabric.ScreenInputDelegate;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.MouseHandler;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.world.InteractionResult;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+@Mixin(MouseHandler.class)
+public class MixinMouseHandler {
+    @Shadow
+    @Final
+    private Minecraft minecraft;
+    
+    @Shadow
+    private int activeButton;
+    
+    @Shadow
+    private double xpos;
+    
+    @Shadow
+    private double ypos;
+    
+    @Inject(method = "onScroll",
+            at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;mouseScrolled(DDD)Z",
+                     ordinal = 0), cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD)
+    public void onMouseScrolled(long long_1, double double_1, double double_2, CallbackInfo info, double amount, double x, double y) {
+        if (!info.isCancelled()) {
+            InteractionResult result = ClientScreenInputEvent.MOUSE_SCROLLED_PRE.invoker().mouseScrolled(minecraft, minecraft.screen, x, y, amount);
+            if (result != InteractionResult.PASS)
+                info.cancel();
+        }
+    }
+    
+    @Inject(method = "onScroll",
+            at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/Screen;mouseScrolled(DDD)Z",
+                     ordinal = 0, shift = At.Shift.AFTER), cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD)
+    public void onMouseScrolledPost(long long_1, double double_1, double double_2, CallbackInfo info, double amount, double x, double y) {
+        if (!info.isCancelled()) {
+            InteractionResult result = ClientScreenInputEvent.MOUSE_SCROLLED_POST.invoker().mouseScrolled(minecraft, minecraft.screen, x, y, amount);
+        }
+    }
+    
+    @Inject(method = "onPress", at = @At(value = "INVOKE",
+                                         target = "Lnet/minecraft/client/gui/screens/Screen;wrapScreenError(Ljava/lang/Runnable;Ljava/lang/String;Ljava/lang/String;)V",
+                                         ordinal = 0), cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD)
+    public void onMouseClicked(long long_1, int button, int int_2, int int_3, CallbackInfo info, boolean bl, int i, boolean[] bls, double d, double e) {
+        if (!info.isCancelled()) {
+            InteractionResult result = ClientScreenInputEvent.MOUSE_CLICKED_PRE.invoker().mouseClicked(minecraft, minecraft.screen, d, e, button);
+            if (result != InteractionResult.PASS)
+                info.cancel();
+        }
+    }
+    
+    @Inject(method = "onPress", at = @At(value = "INVOKE",
+                                         target = "Lnet/minecraft/client/gui/screens/Screen;wrapScreenError(Ljava/lang/Runnable;Ljava/lang/String;Ljava/lang/String;)V",
+                                         ordinal = 0, shift = At.Shift.AFTER), cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD)
+    public void onMouseClickedPost(long long_1, int button, int int_2, int int_3, CallbackInfo info, boolean bl, int i, boolean[] bls, double d, double e) {
+        if (!info.isCancelled()) {
+            InteractionResult result = ClientScreenInputEvent.MOUSE_CLICKED_POST.invoker().mouseClicked(minecraft, minecraft.screen, d, e, button);
+            if (result != InteractionResult.PASS)
+                info.cancel();
+        }
+    }
+    
+    @SuppressWarnings("UnresolvedMixinReference")
+    @Inject(method = {"lambda$onPress$1", "method_1605"}, at = @At("HEAD"), cancellable = true, remap = false)
+    public void onGuiMouseReleased(boolean[] bls, double d, double e, int button, CallbackInfo info) {
+        if (!info.isCancelled()) {
+            InteractionResult result = ClientScreenInputEvent.MOUSE_RELEASED_PRE.invoker().mouseReleased(minecraft, minecraft.screen, d, e, button);
+            if (result != InteractionResult.PASS) {
+                bls[0] = true;
+                info.cancel();
+            }
+        }
+    }
+    
+    @SuppressWarnings("UnresolvedMixinReference")
+    @Inject(method = {"lambda$onPress$1", "method_1605"}, at = @At("RETURN"), cancellable = true, remap = false)
+    public void onGuiMouseReleasedPost(boolean[] bls, double d, double e, int button, CallbackInfo info) {
+        if (!info.isCancelled() && !bls[0]) {
+            InteractionResult result = ClientScreenInputEvent.MOUSE_RELEASED_POST.invoker().mouseReleased(minecraft, minecraft.screen, d, e, button);
+            if (result != InteractionResult.PASS) {
+                bls[0] = true;
+                info.cancel();
+            }
+        }
+    }
+    
+    @SuppressWarnings("UnresolvedMixinReference")
+    @ModifyVariable(method = {"method_1602", "lambda$onMove$11"}, at = @At("HEAD"), ordinal = 0, argsOnly = true)
+    private GuiEventListener wrapMouseDragged(GuiEventListener screen) {
+        if (screen instanceof ScreenInputDelegate) {
+            return ((ScreenInputDelegate) screen).architectury_delegateInputs();
+        }
+        return screen;
+    }
+}

+ 13 - 1
fabric/src/main/java/me/shedaniel/architectury/mixin/fabric/client/MixinScreen.java

@@ -25,6 +25,7 @@ import me.shedaniel.architectury.event.events.TooltipEvent;
 import me.shedaniel.architectury.event.events.client.ClientChatEvent;
 import me.shedaniel.architectury.impl.TooltipEventColorContextImpl;
 import me.shedaniel.architectury.impl.TooltipEventPositionContextImpl;
+import me.shedaniel.architectury.impl.fabric.ScreenInputDelegate;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.gui.components.AbstractWidget;
 import net.minecraft.client.gui.components.events.GuiEventListener;
@@ -42,7 +43,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 import java.util.List;
 
 @Mixin(Screen.class)
-public abstract class MixinScreen {
+public abstract class MixinScreen implements ScreenInputDelegate {
     @Shadow @Final public List<AbstractWidget> buttons;
     @Unique private static ThreadLocal<TooltipEventPositionContextImpl> tooltipPositionContext = ThreadLocal.withInitial(TooltipEventPositionContextImpl::new);
     @Unique private static ThreadLocal<TooltipEventColorContextImpl> tooltipColorContext = ThreadLocal.withInitial(TooltipEventColorContextImpl::new);
@@ -50,6 +51,17 @@ public abstract class MixinScreen {
     @Shadow
     public abstract List<? extends GuiEventListener> children();
     
+    @Unique
+    private GuiEventListener inputDelegate;
+    
+    @Override
+    public GuiEventListener architectury_delegateInputs() {
+        if (inputDelegate == null) {
+            inputDelegate = new DelegateScreen((Screen) (Object) this);
+        }
+        return inputDelegate;
+    }
+    
     @Inject(method = "init(Lnet/minecraft/client/Minecraft;II)V", at = @At(value = "INVOKE", target = "Ljava/util/List;clear()V", ordinal = 0),
             cancellable = true)
     private void preInit(Minecraft minecraft, int i, int j, CallbackInfo ci) {

+ 2 - 1
fabric/src/main/resources/architectury.mixins.json

@@ -5,7 +5,8 @@
   "minVersion": "0.7.11",
   "client": [
     "client.MixinClientLevel", "client.MixinClientPacketListener", "client.MixinDebugScreenOverlay", "client.MixinGameRenderer", "client.MixinIntegratedServer",
-    "client.MixinMinecraft", "client.MixinMultiPlayerGameMode", "client.MixinScreen", "client.MixinTextureAtlas"
+    "client.MixinKeyboardHandler", "client.MixinMinecraft", "client.MixinMouseHandler", "client.MixinMultiPlayerGameMode", "client.MixinScreen",
+    "client.MixinTextureAtlas"
   ],
   "mixins": [
     "ExplosionPreInvoker", "LivingDeathInvoker", "MixinBlockItem", "MixinCommands", "MixinDedicatedServer", "MixinExplosion", "MixinFurnaceResultSlot",

+ 85 - 3
forge/src/main/java/me/shedaniel/architectury/event/forge/EventHandlerImplClient.java

@@ -22,9 +22,7 @@ package me.shedaniel.architectury.event.forge;
 import me.shedaniel.architectury.event.events.TextureStitchEvent;
 import me.shedaniel.architectury.event.events.*;
 import me.shedaniel.architectury.event.events.client.ClientChatEvent;
-import me.shedaniel.architectury.event.events.client.ClientLifecycleEvent;
-import me.shedaniel.architectury.event.events.client.ClientPlayerEvent;
-import me.shedaniel.architectury.event.events.client.ClientTickEvent;
+import me.shedaniel.architectury.event.events.client.*;
 import me.shedaniel.architectury.impl.TooltipEventColorContextImpl;
 import me.shedaniel.architectury.impl.TooltipEventPositionContextImpl;
 import net.minecraft.client.Minecraft;
@@ -182,6 +180,90 @@ public class EventHandlerImplClient {
         event.setBorderStart(colorContext.getOutlineGradientTopColor());
     }
     
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseScrollEvent.Pre event) {
+        if (ClientScreenInputEvent.MOUSE_SCROLLED_PRE.invoker().mouseScrolled(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getScrollDelta()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseScrollEvent.Post event) {
+        ClientScreenInputEvent.MOUSE_SCROLLED_POST.invoker().mouseScrolled(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getScrollDelta());
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseClickedEvent.Pre event) {
+        if (ClientScreenInputEvent.MOUSE_CLICKED_PRE.invoker().mouseClicked(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getButton()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseClickedEvent.Post event) {
+        ClientScreenInputEvent.MOUSE_CLICKED_POST.invoker().mouseClicked(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getButton());
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseDragEvent.Pre event) {
+        if (ClientScreenInputEvent.MOUSE_DRAGGED_PRE.invoker().mouseDragged(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getMouseButton(), event.getDragX(), event.getDragY()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseDragEvent.Post event) {
+        ClientScreenInputEvent.MOUSE_DRAGGED_POST.invoker().mouseDragged(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getMouseButton(), event.getDragX(), event.getDragY());
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseReleasedEvent.Pre event) {
+        if (ClientScreenInputEvent.MOUSE_RELEASED_PRE.invoker().mouseReleased(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getButton()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.MouseReleasedEvent.Post event) {
+        ClientScreenInputEvent.MOUSE_RELEASED_PRE.invoker().mouseReleased(Minecraft.getInstance(), event.getGui(), event.getMouseX(), event.getMouseY(), event.getButton());
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.KeyboardCharTypedEvent.Pre event) {
+        if (ClientScreenInputEvent.CHAR_TYPED_PRE.invoker().charTyped(Minecraft.getInstance(), event.getGui(), event.getCodePoint(), event.getModifiers()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.KeyboardCharTypedEvent.Post event) {
+        ClientScreenInputEvent.CHAR_TYPED_POST.invoker().charTyped(Minecraft.getInstance(), event.getGui(), event.getCodePoint(), event.getModifiers());
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.KeyboardKeyPressedEvent.Pre event) {
+        if (ClientScreenInputEvent.KEY_PRESSED_PRE.invoker().keyPressed(Minecraft.getInstance(), event.getGui(), event.getKeyCode(), event.getScanCode(), event.getModifiers()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.KeyboardKeyPressedEvent.Post event) {
+        ClientScreenInputEvent.KEY_PRESSED_POST.invoker().keyPressed(Minecraft.getInstance(), event.getGui(), event.getKeyCode(), event.getScanCode(), event.getModifiers());
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.KeyboardKeyReleasedEvent.Pre event) {
+        if (ClientScreenInputEvent.KEY_RELEASED_PRE.invoker().keyReleased(Minecraft.getInstance(), event.getGui(), event.getKeyCode(), event.getScanCode(), event.getModifiers()) == InteractionResult.FAIL) {
+            event.setCanceled(true);
+        }
+    }
+    
+    @SubscribeEvent
+    public static void event(GuiScreenEvent.KeyboardKeyReleasedEvent.Post event) {
+        ClientScreenInputEvent.KEY_RELEASED_POST.invoker().keyReleased(Minecraft.getInstance(), event.getGui(), event.getKeyCode(), event.getScanCode(), event.getModifiers());
+    }
+    
     @OnlyIn(Dist.CLIENT)
     public static class ModBasedEventHandler {
         @SubscribeEvent