@@ -32,7 +32,9 @@ import com.lambda.gui.MenuBar
3232import com.lambda.gui.MenuBar.buildMenuBar
3333import com.lambda.gui.OverlayBackgroundScreen
3434import com.lambda.gui.components.QuickSearch.renderQuickSearch
35+ import com.lambda.gui.dsl.ImGuiBuilder
3536import com.lambda.gui.dsl.ImGuiBuilder.buildLayout
37+ import com.lambda.gui.snap.Guide
3638import com.lambda.gui.snap.RectF
3739import com.lambda.gui.snap.SnapHandler
3840import com.lambda.gui.snap.SnapHandler.drawDragGrid
@@ -65,6 +67,7 @@ import net.minecraft.client.gui.screen.ingame.SignEditScreen
6567import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen
6668import net.minecraft.client.util.Icons
6769import java.awt.Color
70+ import kotlin.math.abs
6871
6972@Suppress(" unused" )
7073object 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
0 commit comments