Skip to content

Commit 8d8b37d

Browse files
committed
Allow module window resizing with snapping
1 parent b61912b commit 8d8b37d

2 files changed

Lines changed: 154 additions & 48 deletions

File tree

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

Lines changed: 116 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import com.lambda.gui.MenuBar
3232
import com.lambda.gui.MenuBar.buildMenuBar
3333
import com.lambda.gui.OverlayBackgroundScreen
3434
import com.lambda.gui.components.QuickSearch.renderQuickSearch
35+
import com.lambda.gui.dsl.ImGuiBuilder
3536
import com.lambda.gui.dsl.ImGuiBuilder.buildLayout
37+
import com.lambda.gui.snap.Guide
3638
import com.lambda.gui.snap.RectF
3739
import com.lambda.gui.snap.SnapHandler
3840
import com.lambda.gui.snap.SnapHandler.drawDragGrid
@@ -65,6 +67,7 @@ import net.minecraft.client.gui.screen.ingame.SignEditScreen
6567
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen
6668
import net.minecraft.client.util.Icons
6769
import java.awt.Color
70+
import kotlin.math.abs
6871

6972
@Suppress("unused")
7073
object ClickGuiLayout : Loadable, Config(
@@ -73,8 +76,6 @@ object ClickGuiLayout : Loadable, Config(
7376
) {
7477
var open = false
7578
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.
7879
val keybind by setting("Keybind", KeyCode.Y, screenCheck = false)
7980
.onPressUnsafe {
8081
if (DearImGui.io.wantTextInput) return@onPressUnsafe
@@ -100,10 +101,17 @@ object ClickGuiLayout : Loadable, Config(
100101
private var frameCount = 0
101102
private var activeDragWindowName: String? = null
102103
private var mouseWasDown = false
103-
private var mousePressedThisFrameGlobal = false
104104
private var dragOffsetX = 0f
105105
private var dragOffsetY = 0f
106106
private val lastBounds = mutableMapOf<String, RectF>()
107+
private val pendingSizes = mutableMapOf<String, Pair<Float, Float>>()
108+
private var activeResizeWindowName: String? = null
109+
private var resizeLeftEdge = false
110+
private var resizeRightEdge = false
111+
private var resizeTopEdge = false
112+
private var resizeBottomEdge = false
113+
private var resizeGrabOffsetX = 0f
114+
private var resizeGrabOffsetY = 0f
107115
private val pendingPositions = mutableMapOf<String, Pair<Float, Float>>()
108116
private val snapOverlays = mutableMapOf<String, SnapHandler.SnapVisual>()
109117

@@ -272,30 +280,32 @@ object ClickGuiLayout : Loadable, Config(
272280
SnapHandler.beginFrame(vp.sizeX, vp.sizeY, io.fontGlobalScale)
273281

274282
val mouseDown = io.mouseDown[0]
275-
val mousePressedThisFrame = mouseDown && !mouseWasDown
276283
val mouseReleasedThisFrame = !mouseDown && mouseWasDown
277284
mouseWasDown = mouseDown
278-
mousePressedThisFrameGlobal = mousePressedThisFrame
279285

280286
if (mouseReleasedThisFrame) {
281287
activeDragWindowName = null
288+
activeResizeWindowName?.let { pendingSizes.remove(it) }
289+
activeResizeWindowName = null
282290
}
283291

284292
pendingPositions.clear()
285293
snapOverlays.clear()
286294

287-
val mouseDownGlobal = io.mouseDown[0]
288-
activeDragWindowName?.let { drag ->
289-
if (mouseDownGlobal) {
290-
updateDragAndSnapping(
291-
drag,
292-
lastBounds[activeDragWindowName]!!,
293-
dragOffsetX,
294-
dragOffsetY,
295-
pendingPositions,
296-
snapOverlays
297-
)
298-
drawDragGrid()
295+
if (mouseDown) {
296+
activeDragWindowName?.let { name ->
297+
lastBounds[name]?.let { bounds ->
298+
updateDragAndSnapping(
299+
name, bounds, dragOffsetX, dragOffsetY, pendingPositions, snapOverlays
300+
)
301+
drawDragGrid()
302+
}
303+
}
304+
activeResizeWindowName?.let { name ->
305+
lastBounds[name]?.let { bounds ->
306+
updateResizeAndSnapping(name, bounds)
307+
drawDragGrid()
308+
}
299309
}
300310
}
301311

@@ -306,49 +316,29 @@ object ClickGuiLayout : Loadable, Config(
306316
val baseY = MenuBar.height + 10f
307317

308318
tags.forEach { tag ->
309-
val mouseDownGlobal = io.mouseDown[0]
310-
activeDragWindowName?.let { drag ->
311-
if (mouseDownGlobal) {
312-
updateDragAndSnapping(
313-
drag, lastBounds[drag]!!, dragOffsetX, dragOffsetY, pendingPositions, snapOverlays
314-
)
315-
}
316-
}
317-
318319
val override = pendingPositions[tag.name]
319320
if (override != null) {
320321
ImGui.setNextWindowPos(override.first, override.second)
321322
} else if (frameCount >= 1) {
322323
ImGui.setNextWindowPos(nextX, baseY, ImGuiCond.FirstUseEver)
323324
}
324325

325-
// FixMe:
326-
// Due to the auto resize of windows, if a tag has no module names that is at least the
327-
// same length as the tag name, the title of the window will clip out the window box.
328-
// For the time being I have removed the ability to collapse the windows so the titles
329-
// have more space lol.
330-
window(tag.name, flags = ImGuiWindowFlags.AlwaysAutoResize or ImGuiWindowFlags.NoCollapse) {
331-
if (activeDragWindowName == null && mousePressedThisFrameGlobal && ImGui.isWindowHovered()) {
332-
val mx = io.mousePos.x
333-
val my = io.mousePos.y
334-
val titleBarHeight = ImGui.getFrameHeight()
335-
if (my >= windowPos.y && my <= windowPos.y + titleBarHeight) {
336-
activeDragWindowName = tag.name
337-
dragOffsetX = mx - windowPos.x
338-
dragOffsetY = my - windowPos.y
339-
}
340-
}
326+
pendingSizes[tag.name]?.let { (w, h) ->
327+
ImGui.setNextWindowSizeConstraints(w, h, w, h)
328+
}
341329

330+
window(tag.name, flags = ImGuiWindowFlags.NoCollapse) {
342331
ModuleRegistry.modules
343332
.filter { it.tag == tag && it.showInClickGui.value }
344333
.forEach { with(ModuleEntry(it)) { buildLayout() } }
345334

346-
val vis = snapOverlays[tag.name]
347-
if (vis != null) {
335+
snapOverlays[tag.name]?.let { vis ->
348336
drawSnapLines(vis.snapX, vis.kindX, vis.snapY, vis.kindY)
349337
}
350338

351339
val rect = RectF(windowPos.x, windowPos.y, windowSize.x, windowSize.y)
340+
if (mouseDown) claimInteraction(tag.name, rect)
341+
352342
SnapHandler.registerElement(tag.name, rect)
353343
lastBounds[tag.name] = rect
354344

@@ -370,6 +360,88 @@ object ClickGuiLayout : Loadable, Config(
370360
}
371361
}
372362

363+
private fun ImGuiBuilder.claimInteraction(name: String, rect: RectF) {
364+
if (activeDragWindowName != null || activeResizeWindowName != null) return
365+
val previous = lastBounds[name] ?: return
366+
367+
val movedX = abs(rect.x - previous.x) > 0.5f
368+
val movedY = abs(rect.y - previous.y) > 0.5f
369+
val sizedX = abs(rect.w - previous.w) > 0.5f
370+
val sizedY = abs(rect.h - previous.h) > 0.5f
371+
372+
when {
373+
sizedX || sizedY -> {
374+
val grip = ImGui.getFontSize() * 1.35f
375+
val toLeft = io.mousePos.x - rect.left
376+
val toRight = rect.right - io.mousePos.x
377+
val toTop = io.mousePos.y - rect.top
378+
val toBottom = rect.bottom - io.mousePos.y
379+
380+
activeResizeWindowName = name
381+
resizeLeftEdge = toLeft <= grip && toLeft < toRight
382+
resizeRightEdge = toRight <= grip && toRight <= toLeft
383+
resizeTopEdge = toTop <= grip && toTop < toBottom
384+
resizeBottomEdge = toBottom <= grip && toBottom <= toTop
385+
386+
if (sizedX && !resizeLeftEdge && !resizeRightEdge) resizeRightEdge = true
387+
if (sizedY && !resizeTopEdge && !resizeBottomEdge) resizeBottomEdge = true
388+
389+
resizeGrabOffsetX = io.mousePos.x - if (resizeLeftEdge) rect.left else rect.right
390+
resizeGrabOffsetY = io.mousePos.y - if (resizeTopEdge) rect.top else rect.bottom
391+
}
392+
movedX || movedY -> {
393+
activeDragWindowName = name
394+
dragOffsetX = io.mousePos.x - rect.x
395+
dragOffsetY = io.mousePos.y - rect.y
396+
}
397+
}
398+
}
399+
400+
private fun ImGuiBuilder.updateResizeAndSnapping(name: String, bounds: RectF) {
401+
var x = bounds.x
402+
var y = bounds.y
403+
var width = bounds.w
404+
var height = bounds.h
405+
var snapX: Float? = null
406+
var snapY: Float? = null
407+
var kindX: Guide.Kind? = null
408+
var kindY: Guide.Kind? = null
409+
410+
fun snapped(offset: Float, mouse: Float, orientation: Guide.Orientation): Pair<Float, Guide.Kind?> {
411+
val proposed = mouse - offset
412+
val snap = SnapHandler.snapEdge(proposed, orientation, name)
413+
return (snap.pos ?: proposed) to snap.kind
414+
}
415+
416+
if (resizeLeftEdge || resizeRightEdge) {
417+
val (edge, kind) = snapped(resizeGrabOffsetX, io.mousePos.x, Guide.Orientation.Vertical)
418+
if (resizeLeftEdge) {
419+
x = edge.coerceAtMost(bounds.right - style.windowMinSize.x)
420+
width = bounds.right - x
421+
} else {
422+
width = (edge - bounds.left).coerceAtLeast(style.windowMinSize.x)
423+
}
424+
snapX = edge
425+
kindX = kind
426+
}
427+
428+
if (resizeTopEdge || resizeBottomEdge) {
429+
val (edge, kind) = snapped(resizeGrabOffsetY, io.mousePos.y, Guide.Orientation.Horizontal)
430+
if (resizeTopEdge) {
431+
y = edge.coerceAtMost(bounds.bottom - style.windowMinSize.y)
432+
height = bounds.bottom - y
433+
} else {
434+
height = (edge - bounds.top).coerceAtLeast(style.windowMinSize.y)
435+
}
436+
snapY = edge
437+
kindY = kind
438+
}
439+
440+
pendingSizes[name] = width to height
441+
if (resizeLeftEdge || resizeTopEdge) pendingPositions[name] = x to y
442+
snapOverlays[name] = SnapHandler.SnapVisual(snapX, snapY, kindX, kindY)
443+
}
444+
373445
val Screen?.hasInput: Boolean
374446
get() = this is ChatScreen ||
375447
this is SignEditScreen ||
@@ -378,16 +450,12 @@ object ClickGuiLayout : Loadable, Config(
378450

379451
fun toggle() {
380452
if (open) {
381-
// LambdaScreen.close() restores the background screen and triggers
382-
// removed() -> close(), which flips `open` off and plays the sound.
383453
LambdaScreen.close()
384454
} else {
385455
val current = mc.currentScreen
386456
if (current.hasInput) return
387457
if (Client.clientSounds) LambdaSound.ModuleOn.play()
388458
LambdaScreen.parentScreen = if (current is LambdaScreen) null else current
389-
// Let the parent retain resources past the removed() that setScreen triggers,
390-
// since we restore it when the GUI closes.
391459
(current as? OverlayBackgroundScreen)?.onOverlaidByGui()
392460
mc.setScreen(LambdaScreen)
393461
open = true

src/main/kotlin/com/lambda/gui/snap/SnapHandler.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,44 @@ object SnapHandler : Loadable {
201201
)
202202
}
203203

204+
data class EdgeSnapResult(val pos: Float?, val kind: Guide.Kind?)
205+
206+
fun snapEdge(pos: Float, orientation: Guide.Orientation, currentId: String?): EdgeSnapResult {
207+
class Best {
208+
var s = Float.POSITIVE_INFINITY
209+
var p: Float? = null
210+
var k: Guide.Kind? = null
211+
}
212+
213+
val elem = Best(); val screen = Best(); val grid = Best()
214+
215+
frameGuides.forEach { sg ->
216+
val g = sg.guide
217+
if (g.orientation != orientation) return@forEach
218+
219+
val out = when (g.kind) {
220+
Guide.Kind.ElementEdge, Guide.Kind.ElementCenter ->
221+
if (sg.sourceId == currentId) return@forEach else elem
222+
Guide.Kind.ScreenCenter -> screen
223+
Guide.Kind.Grid -> grid
224+
}
225+
226+
val dist = abs(pos - g.pos)
227+
if (dist > max(1f, thresholdFor(g.kind))) return@forEach
228+
229+
val sc = score(dist, g.strength)
230+
if (sc < out.s) { out.s = sc; out.p = g.pos; out.k = g.kind }
231+
}
232+
233+
val choice = when {
234+
elem.s.isFinite() -> elem
235+
screen.s.isFinite() -> screen
236+
grid.s.isFinite() -> grid
237+
else -> return EdgeSnapResult(null, null)
238+
}
239+
return EdgeSnapResult(choice.p, choice.k)
240+
}
241+
204242
fun ImGuiBuilder.drawSnapLines(snapX: Float?, kindX: Guide.Kind?, snapY: Float?, kindY: Guide.Kind?) {
205243
val draw = foregroundDrawList
206244
val showX = kindX == Guide.Kind.ElementEdge || kindX == Guide.Kind.ElementCenter

0 commit comments

Comments
 (0)