Skip to content

Commit 3fd52bf

Browse files
Feature: Open ClickGui on More Screens (#302)
1 parent 3b5b0a4 commit 3fd52bf

7 files changed

Lines changed: 167 additions & 24 deletions

File tree

src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ open class CollectionSetting<R : Any>(
6565
private var searchFilter = ""
6666

6767
val selectListeners = mutableListOf<SafeContext.(R) -> Unit>()
68+
val unsafeSelectListeners = mutableListOf<(R) -> Unit>()
6869
val deselectListeners = mutableListOf<SafeContext.(R) -> Unit>()
70+
val unsafeDeselectListeners = mutableListOf<(R) -> Unit>()
6971

7072
override val isModified: Boolean
7173
get() = with(originalCore) {
@@ -113,6 +115,7 @@ open class CollectionSetting<R : Any>(
113115
flags = DontClosePopups
114116
) {
115117
value.add(v)
118+
unsafeSelectListeners.forEach { listener -> listener(v) }
116119
runSafe { selectListeners.forEach { listener -> listener(v) } }
117120
}
118121
}
@@ -134,6 +137,8 @@ open class CollectionSetting<R : Any>(
134137
value.add(item)
135138
}
136139
}
140+
currentlySelected.forEach { v -> unsafeDeselectListeners.forEach { listener -> listener(v) } }
141+
value.forEach { v -> unsafeSelectListeners.forEach { listener -> listener(v) } }
137142
runSafe {
138143
currentlySelected.forEach { v -> deselectListeners.forEach { listener -> listener(v) } }
139144
value.forEach { v -> selectListeners.forEach { listener -> listener(v) } }
@@ -159,6 +164,7 @@ open class CollectionSetting<R : Any>(
159164
flags = DontClosePopups
160165
) {
161166
value.remove(v)
167+
unsafeDeselectListeners.forEach { listener -> listener(v) }
162168
runSafe { deselectListeners.forEach { listener -> listener(v) } }
163169
}
164170
}
@@ -175,10 +181,18 @@ open class CollectionSetting<R : Any>(
175181
fun <T : CollectionSetting<R>, R : Any> T.onSelect(block: SafeContext.(R) -> Unit) =
176182
apply { selectListeners.add(block) }
177183

184+
@ConfigEntryDsl
185+
fun <T : CollectionSetting<R>, R : Any> T.onSelectUnsafe(block: (R) -> Unit) =
186+
apply { unsafeSelectListeners.add(block) }
187+
178188
@ConfigEntryDsl
179189
fun <T : CollectionSetting<R>, R : Any> T.onDeselect(block: SafeContext.(R) -> Unit) =
180190
apply { deselectListeners.add(block) }
181191

192+
@ConfigEntryDsl
193+
fun <T : CollectionSetting<R>, R : Any> T.onDeselectUnsafe(block: (R) -> Unit) =
194+
apply { unsafeDeselectListeners.add(block) }
195+
182196
@Suppress("unchecked_cast")
183197
@ConfigEditorD5l
184198
fun <T : Any> ConfigEditor.SettingEditBuilder<Collection<T>>.immutableCollection(collection: Collection<T>) {

src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ import com.lambda.brigadier.argument.word
2626
import com.lambda.brigadier.executeWithResult
2727
import com.lambda.brigadier.optional
2828
import com.lambda.brigadier.required
29+
import com.lambda.Lambda.mc
2930
import com.lambda.config.Config
3031
import com.lambda.config.entries.ConfigEntryDsl
3132
import com.lambda.config.entries.Setting
3233
import com.lambda.config.entries.SettingEntryLayer
3334
import com.lambda.context.SafeContext
3435
import com.lambda.event.Muteable
3536
import com.lambda.event.events.ButtonEvent
36-
import com.lambda.event.listener.SafeListener.Companion.listen
37+
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
38+
import com.lambda.threading.runSafe
3739
import com.lambda.gui.dsl.ImGuiBuilder
3840
import com.lambda.imgui.ImGui.isMouseClicked
3941
import com.lambda.imgui.flag.ImGuiCol
@@ -78,28 +80,39 @@ class KeybindSetting(
7880
) : this(name, description, config, layer, visibility, Bind(defaultValue.code, 0, -1), muteable, alwaysListen, screenCheck)
7981

8082
private val pressListeners = mutableListOf<SafeContext.(ButtonEvent) -> Unit>()
83+
private val unsafePressListeners = mutableListOf<(ButtonEvent) -> Unit>()
8184
private val repeatListeners = mutableListOf<SafeContext.(ButtonEvent) -> Unit>()
85+
private val unsafeRepeatListeners = mutableListOf<(ButtonEvent) -> Unit>()
8286
private val releaseListeners = mutableListOf<SafeContext.(ButtonEvent) -> Unit>()
87+
private val unsafeReleaseListeners = mutableListOf<(ButtonEvent) -> Unit>()
8388

8489
private var listening = false
8590

8691
override val isMuted
8792
get() = muteable?.isMuted == true && !alwaysListening
8893

8994
init {
90-
listen<ButtonEvent.Keyboard.Press> { event -> onButtonEvent(event) }
91-
listen<ButtonEvent.Mouse.Click> { event -> onButtonEvent(event) }
95+
listenUnsafe<ButtonEvent.Keyboard.Press> { event -> onButtonEvent(event) }
96+
listenUnsafe<ButtonEvent.Mouse.Click> { event -> onButtonEvent(event) }
9297
}
9398

94-
private fun SafeContext.onButtonEvent(event: ButtonEvent) {
99+
private fun onButtonEvent(event: ButtonEvent) {
95100
if (mc.options.commandKey.isPressed ||
96101
(screenCheck && mc.currentScreen != null) ||
97102
!event.satisfies(value)) return
98103

99104
if (event.isPressed) {
100-
if (event.isRepeated) repeatListeners.forEach { it(event) }
101-
else pressListeners.forEach { it(event) }
102-
} else if (event.isReleased) releaseListeners.forEach { it(event) }
105+
if (event.isRepeated) {
106+
unsafeRepeatListeners.forEach { it(event) }
107+
runSafe { repeatListeners.forEach { it(event) } }
108+
} else {
109+
unsafePressListeners.forEach { it(event) }
110+
runSafe { pressListeners.forEach { it(event) } }
111+
}
112+
} else if (event.isReleased) {
113+
unsafeReleaseListeners.forEach { it(event) }
114+
runSafe { releaseListeners.forEach { it(event) } }
115+
}
103116
}
104117

105118
override fun ImGuiBuilder.buildLayout() {
@@ -211,11 +224,20 @@ class KeybindSetting(
211224
@ConfigEntryDsl
212225
fun KeybindSetting.onPress(block: SafeContext.(ButtonEvent) -> Unit) = apply { pressListeners.add(block) }
213226

227+
@ConfigEntryDsl
228+
fun KeybindSetting.onPressUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafePressListeners.add(block) }
229+
214230
@ConfigEntryDsl
215231
fun KeybindSetting.onRepeat(block: SafeContext.(ButtonEvent) -> Unit) = apply { repeatListeners.add(block) }
216232

233+
@ConfigEntryDsl
234+
fun KeybindSetting.onRepeatUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafeRepeatListeners.add(block) }
235+
217236
@ConfigEntryDsl
218237
fun KeybindSetting.onRelease(block: SafeContext.(ButtonEvent) -> Unit) = apply { releaseListeners.add(block) }
238+
239+
@ConfigEntryDsl
240+
fun KeybindSetting.onReleaseUnsafe(block: (ButtonEvent) -> Unit) = apply { unsafeReleaseListeners.add(block) }
219241
}
220242
}
221243

src/main/kotlin/com/lambda/gui/DearImGui.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ object DearImGui : Loadable {
108108
null
109109
)
110110

111+
// When the GUI is open over a menu screen, the in-game world blur (applied via the
112+
// screen's GUI render layers) isn't available, so blur the whole framebuffer here —
113+
// after the parent screen has fully rendered but before ImGui draws on top.
114+
if (ClickGuiLayout.open && ClickGuiLayout.backgroundBlur && LambdaScreen.parentScreen != null) {
115+
mc.gameRenderer.renderBlur()
116+
}
117+
111118
GlStateManager._glBindFramebuffer(GL_FRAMEBUFFER, prevFramebuffer)
112119

113120
implGlfw.newFrame()

src/main/kotlin/com/lambda/gui/LambdaScreen.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,43 @@ import net.minecraft.client.gui.screen.Screen
2323
import net.minecraft.text.Text
2424

2525
object LambdaScreen : Screen(Text.of("Lambda")) {
26+
/**
27+
* The screen the GUI was opened over (e.g. the title screen).
28+
* Null when opened in-game; in that case closing returns to gameplay.
29+
*/
30+
var parentScreen: Screen? = null
31+
2632
override fun shouldPause() = false
2733
override fun removed() = ClickGuiLayout.close()
2834
override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, deltaTicks: Float) {}
2935

36+
override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
37+
if (parentScreen == null) {
38+
// In-game: keep vanilla behavior (blur + darken the rendered world).
39+
super.renderBackground(context, mouseX, mouseY, delta)
40+
return
41+
}
42+
// Off-screen mouse coords keep the parent's widgets from showing a hover state.
43+
parentScreen?.renderBackground(context, -1, -1, delta)
44+
parentScreen?.render(context, -1, -1, delta)
45+
// Flush deferred elements (e.g. widget text) so the darkening overlay covers them.
46+
context.drawDeferredElements()
47+
// NOTE: no GUI-layer applyBlur() here. MC's layer blur only blurs background
48+
// layers, never foreground widgets. To blur the whole parent (widgets included)
49+
// we run a framebuffer-level blur in DearImGui.render() after the full GUI pass.
50+
}
51+
52+
override fun resize(width: Int, height: Int) {
53+
super.resize(width, height)
54+
parentScreen?.resize(width, height)
55+
}
56+
57+
override fun close() {
58+
val previous = parentScreen
59+
parentScreen = null
60+
client?.setScreen(previous)
61+
}
62+
3063
override fun applyBlur(context: DrawContext?) {
3164
if (!ClickGuiLayout.backgroundBlur) return
3265
super.applyBlur(context)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2026 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.gui
19+
20+
/**
21+
* Implemented by screens the click GUI can open over that hold resources (e.g. textures)
22+
* which must survive being temporarily replaced by [LambdaScreen].
23+
*
24+
* Opening the GUI calls [net.minecraft.client.MinecraftClient.setScreen], which fires
25+
* [net.minecraft.client.gui.screen.Screen.removed] on the screen being overlaid — screens
26+
* that free resources there would lose them even though the GUI restores the screen on close.
27+
* [onOverlaidByGui] is invoked just before that happens so the screen can retain its resources.
28+
*/
29+
interface OverlayBackgroundScreen {
30+
fun onOverlaidByGui()
31+
}

src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import com.lambda.config.Config
2222
import com.lambda.config.Tab
2323
import com.lambda.config.categories.GuiCategory
2424
import com.lambda.config.entries.Setting.Companion.onValueChange
25-
import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
25+
import com.lambda.config.settings.complex.KeybindSetting.Companion.onPressUnsafe
2626
import com.lambda.core.Loadable
2727
import com.lambda.event.events.GuiEvent
28-
import com.lambda.event.listener.SafeListener.Companion.listen
28+
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
2929
import com.lambda.gui.DearImGui
3030
import com.lambda.gui.LambdaScreen
31+
import com.lambda.gui.OverlayBackgroundScreen
3132
import com.lambda.gui.MenuBar
3233
import com.lambda.gui.MenuBar.buildMenuBar
3334
import com.lambda.gui.components.QuickSearch.renderQuickSearch
@@ -45,6 +46,7 @@ import com.lambda.imgui.flag.ImGuiHoveredFlags
4546
import com.lambda.imgui.flag.ImGuiWindowFlags
4647
import com.lambda.module.ModuleRegistry
4748
import com.lambda.module.modules.client.Client
49+
import com.lambda.module.modules.combat.autodisconnect.AutoDisconnectScreen
4850
import com.lambda.module.tag.ModuleTag
4951
import com.lambda.module.tag.ModuleTag.Companion.shownTags
5052
import com.lambda.sound.LambdaSound
@@ -56,9 +58,11 @@ import com.lambda.util.WindowUtils.setLambdaWindowIcon
5658
import net.minecraft.SharedConstants
5759
import net.minecraft.client.gui.screen.ChatScreen
5860
import net.minecraft.client.gui.screen.Screen
61+
import net.minecraft.client.gui.screen.TitleScreen
5962
import net.minecraft.client.gui.screen.ingame.AnvilScreen
6063
import net.minecraft.client.gui.screen.ingame.CommandBlockScreen
6164
import net.minecraft.client.gui.screen.ingame.SignEditScreen
65+
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen
6266
import net.minecraft.client.util.Icons
6367
import java.awt.Color
6468

@@ -69,12 +73,29 @@ object ClickGuiLayout : Loadable, Config(
6973
) {
7074
var open = false
7175
var developerMode = false
76+
// onPressUnsafe (not onPress) so the GUI can also be toggled from menu screens
77+
// (title, multiplayer, world-select) where there is no SafeContext, not just in-game.
7278
val keybind by setting("Keybind", KeyCode.Y, screenCheck = false)
73-
.onPress {
74-
if (!open && mc.currentScreen != null) return@onPress
75-
if (DearImGui.io.wantTextInput) return@onPress
79+
.onPressUnsafe {
80+
if (DearImGui.io.wantTextInput) return@onPressUnsafe
81+
if (!open && !canOpenOver(mc.currentScreen)) return@onPressUnsafe
7682
toggle()
7783
}
84+
85+
/**
86+
* Screens (besides the in-game/null case) the GUI is allowed to open over.
87+
* Add a class here to support opening the GUI on another screen.
88+
*/
89+
private val backgroundScreenTypes = mutableListOf(
90+
TitleScreen::class.java,
91+
MultiplayerScreen::class.java,
92+
AutoDisconnectScreen::class.java,
93+
)
94+
95+
/** True when the GUI may be opened over [screen]; null means in-game. */
96+
fun canOpenOver(screen: Screen?): Boolean =
97+
screen == null || backgroundScreenTypes.any { it.isInstance(screen) }
98+
7899
private var initialLayoutComplete = false
79100
private var frameCount = 0
80101
private var activeDragWindowName: String? = null
@@ -246,8 +267,8 @@ object ClickGuiLayout : Loadable, Config(
246267
@Tab(COLORS_TAB) val modalWindowDimBg by setting("Modal Window Dim Background", Color(35, 0, 14, 90))
247268

248269
init {
249-
listen<GuiEvent.NewImguiFrame> {
250-
if (!open) return@listen
270+
listenUnsafe<GuiEvent.NewImguiFrame> {
271+
if (!open) return@listenUnsafe
251272

252273
buildLayout {
253274
buildMenuBar()
@@ -361,16 +382,21 @@ object ClickGuiLayout : Loadable, Config(
361382

362383
fun toggle() {
363384
if (open) {
364-
close()
385+
// LambdaScreen.close() restores the background screen and triggers
386+
// removed() -> close(), which flips `open` off and plays the sound.
365387
LambdaScreen.close()
366388
} else {
367-
if (!mc.currentScreen.hasInput) {
368-
if (Client.clientSounds) LambdaSound.ModuleOn.play()
369-
mc.setScreen(LambdaScreen)
370-
open = true
371-
frameCount = 0
372-
initialLayoutComplete = false
373-
}
389+
val current = mc.currentScreen
390+
if (current.hasInput) return
391+
if (Client.clientSounds) LambdaSound.ModuleOn.play()
392+
LambdaScreen.parentScreen = if (current is LambdaScreen) null else current
393+
// Let the parent retain resources past the removed() that setScreen triggers,
394+
// since we restore it when the GUI closes.
395+
(current as? OverlayBackgroundScreen)?.onOverlaidByGui()
396+
mc.setScreen(LambdaScreen)
397+
open = true
398+
frameCount = 0
399+
initialLayoutComplete = false
374400
}
375401
}
376402

src/main/kotlin/com/lambda/module/modules/combat/autodisconnect/AutoDisconnectScreen.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.lambda.module.modules.combat.autodisconnect
1919

20+
import com.lambda.gui.OverlayBackgroundScreen
2021
import com.lambda.util.render.CursorOverrideProvider
2122
import net.minecraft.client.gl.RenderPipelines
2223
import net.minecraft.client.gui.Click
@@ -32,8 +33,11 @@ import net.minecraft.client.gui.widget.ScrollableTextWidget
3233
import net.minecraft.text.Text
3334
import kotlin.math.min
3435

35-
class AutoDisconnectScreen(private val details: DisconnectDetails) :
36-
Screen(Text.literal("Disconnected: ").append(details.reason)) {
36+
class AutoDisconnectScreen(
37+
private val details: DisconnectDetails
38+
) : Screen(Text.literal("Disconnected: ").append(details.reason)),
39+
OverlayBackgroundScreen
40+
{
3741
//state
3842
private val parent = TitleScreen()
3943
private var showDetails = !details.hideDetails
@@ -106,6 +110,12 @@ class AutoDisconnectScreen(private val details: DisconnectDetails) :
106110
releaseTexture()
107111
}
108112

113+
override fun onOverlaidByGui() {
114+
// The click GUI temporarily replaces this screen and restores it on close,
115+
// so keep the screenshot texture instead of releasing it on removed().
116+
keepTextureOnRemove = true
117+
}
118+
109119
private fun openImagePreview() {
110120
keepTextureOnRemove = true
111121
client?.setScreen(ImagePreviewScreen(this, details))

0 commit comments

Comments
 (0)