diff --git a/src/commonMain/kotlin/baaahs/ui/gridlayout/Layout.kt b/src/commonMain/kotlin/baaahs/ui/gridlayout/Layout.kt index 6cbd019d9f..96c471f083 100644 --- a/src/commonMain/kotlin/baaahs/ui/gridlayout/Layout.kt +++ b/src/commonMain/kotlin/baaahs/ui/gridlayout/Layout.kt @@ -2,8 +2,6 @@ package baaahs.ui.gridlayout import baaahs.replaceAll import baaahs.util.Logger -import kotlin.math.max -import kotlin.math.min data class Layout( val items: List, @@ -37,85 +35,8 @@ data class Layout( private fun updateLayout(updateItem: LayoutItem): Layout = Layout(items.map { if (it.i == updateItem.i) updateItem else it }, cols, rows) - /** - * Given a layout, compact it. This involves going down each y coordinate and removing gaps - * between items. - * - * Does not modify layout items (clones). Creates a new layout array. - * - * @param {Array} layout Layout. - * @param {Boolean} verticalCompact Whether or not to compact the layout - * vertically. - * @return {Array} Compacted Layout. - */ - fun compact(compactType: CompactType): Layout { - // Statics go in the compareWith array right away so items flow around them. - val compareWith = getStatics().toMutableList() - // We go through the items by row and column. - val sorted = sortLayoutItems(compactType) - // Holding for new items. - val out = MutableList(items.size) { null } - - val len = sorted.size - for (i in 0 until len) { - var l = sorted[i].copy() - - // Don't move static elements - if (!l.isStatic && compactType != CompactType.None) { - l = compactItem(Layout(compareWith, cols, rows), l, compactType, cols, sorted) - - // Add to comparison array. We only collide with items before this one. - // Statics are already in this array. - compareWith.add(l) - } - - // Add to output array to make sure they still come out in the right order. - out[items.indexOf(sorted[i])] = l - - // Clear moved flag, if it exists. - if (l.moved) { - out.replaceAll { if (it?.i == l.i) l.copy(moved = false) else it } - } - } - - return Layout(out.filterNotNull(), cols, rows) - } - - /** - * Before moving item down, it will check if the movement will cause collisions and move those items down before. - */ - private fun resolveCompactionCollision( - item: LayoutItem, - moveToCoord: Int, - axis: Axis - ): LayoutItem { - var newItem = axis.incr(item) - - val sizeProp = heightWidth[axis]!! - val itemIndex = items.indexOfFirst { layoutItem -> layoutItem.i == newItem.i } - - // Go through each item we collide with. - for (i in itemIndex + 1 until items.size) { - val otherItem = items[i] - // Ignore static items - if (otherItem.isStatic) continue - - // Optimization: we can break early if we know we're past this el - // We can do this b/c it's a sorted layout - if (otherItem.y > newItem.y + newItem.h) break - - if (newItem.collidesWith(otherItem)) { - newItem = - resolveCompactionCollision( - otherItem, - moveToCoord + sizeProp.invoke(newItem), - axis - ) - } - } - - return axis.set(newItem, moveToCoord) - } + fun resetMovedFlag(): Layout = + Layout(items.map { it.copy(moved = false) }, cols, rows) /** * Given a layout, make sure all elements fit within its bounds. @@ -175,7 +96,7 @@ data class Layout( private fun anyCollisions(layoutItem: LayoutItem): Boolean = items.any { it.collidesWith(layoutItem) } - fun getAllCollisions(layoutItem: LayoutItem): List = + fun findCollisions(layoutItem: LayoutItem): List = items.filter { l -> l.collidesWith(layoutItem) } /** @@ -189,7 +110,7 @@ data class Layout( /** * Move an element. Responsible for doing cascading movements of other elements. * - * Modifies layout items. + * Returns a new layout with moved layout items. * * @param {Array} layout Full layout to modify. * @param {LayoutItem} l element to move. @@ -199,27 +120,28 @@ data class Layout( fun moveElement( l: LayoutItem, x: Int, - y: Int, - preventCollision: Boolean, - compactType: CompactType, - allowOverlap: Boolean = false - ): Layout = - try { - moveElementInternal(l, x, y, true, preventCollision, compactType, allowOverlap) - } catch (e: OutOfBoundsException) { this } + y: Int + ): Layout { + for (direction in Direction.rankedPushOptions(x - l.x, y - l.y)) { + try { + return moveElementInternal(l, x, y, true, direction) + } catch (e: ImpossibleLayoutException) { + // Try again. + } + } + throw ImpossibleLayoutException() + } private fun moveElementInternal( l: LayoutItem, - x: Int?, - y: Int?, + x: Int, + y: Int, isDirectUserAction: Boolean, - preventCollision: Boolean, - compactType: CompactType, - allowOverlap: Boolean = false + pushDirection: Direction ): Layout { // If this is static and not explicitly enabled as draggable, // no move is possible, so we can short-circuit this immediately. - if (l.isStatic && !l.isDraggable) return this + if (l.isStatic && !l.isDraggable) throw ImpossibleLayoutException() // Short-circuit if nothing to do. if (l.y == y && l.x == x) return this @@ -227,66 +149,50 @@ data class Layout( logger.debug { "Moving element ${l.i} to [$x,$y] from [${l.x},${l.y}]" } - val oldX = l.x - val oldY = l.y - - val newItem = l.movedTo(x, y) - if (newItem.right + 1 > cols || newItem.bottom + 1 > rows) - throw OutOfBoundsException() - - var updatedLayout = updateLayout(newItem) - - // If this collides with anything, move it. - // When doing this comparison, we have to sort the items we compare with - // to ensure, in the case of multiple collisions, that we're getting the - // nearest collision. - var sorted = updatedLayout.sortLayoutItems(compactType) - val movingUp = - if (compactType.isVertical && y != null) - oldY >= y - else if (compactType.isHorizontal && x != null) - oldX >= x - else false - // $FlowIgnore acceptable modification of read-only array as it was recently cloned - if (movingUp) sorted = Layout(sorted.items.reversed(), cols, rows) - val collisions = sorted.getAllCollisions(newItem) - val hasCollisions = collisions.isNotEmpty() - - // We may have collisions. We can short-circuit if we've turned off collisions or - // allowed overlap. - if (hasCollisions && allowOverlap) { - // Easy, we don't need to resolve collisions. But we *did* change the layout, - // so clone it on the way out. - return updatedLayout - } else if (hasCollisions && preventCollision) { - // If we are preventing collision but not allowing overlap, we need to - // revert the position of this element so it goes to where it came from, rather - // than the user's desired location. - logger.info { "Collision prevented on ${l.i}, reverting." } - return this // did not change so don't clone - } - // Move each item that collides away from this element. - for (collision in collisions) { - logger.info { - "Resolving collision between ${newItem.i} at [${newItem.x},${newItem.y}] and ${collision.i} at [${collision.x},${collision.y}]" + val movedItem = l.movedTo(x, y) + if (outOfBounds(movedItem)) + throw ImpossibleLayoutException() + + var updatedLayout = updateLayout(movedItem) + val collisions = findCollisions(movedItem) + + // If it collides with anything, move it (recursively). + if (collisions.isNotEmpty()) { + // When doing this comparison, we have to sort the items we compare with + // to ensure, in the case of multiple collisions, that we're getting the + // nearest collision. + for (collision in pushDirection.sort(collisions)) { + logger.info { + "Resolving collision between ${movedItem.i} at [${movedItem.x},${movedItem.y}] and ${collision.i} at [${collision.x},${collision.y}]" + } + + // Short circuit so we can't infinitely loop + if (collision.moved) throw ImpossibleLayoutException() + + updatedLayout = + updatedLayout.pushCollidingElement(movedItem, collision, isDirectUserAction, pushDirection) } + } + + return updatedLayout + } - // Short circuit so we can't infinitely loop - if (collision.moved) continue + fun resizeElement(item: LayoutItem, w: Int, h: Int): Layout { + val resizedItem = item.copy(w = w, h = h) + var updatedLayout = updateLayout(resizedItem) + val collisions = updatedLayout.findCollisions(resizedItem) - // Don't move static items - we have to move *this* element away - updatedLayout = - if (collision.isStatic) { - updatedLayout.moveElementAwayFromCollision(collision, newItem, isDirectUserAction, compactType) - } else { - updatedLayout.moveElementAwayFromCollision(newItem, collision, isDirectUserAction, compactType) - } + if (collisions.isNotEmpty()) { + throw ImpossibleLayoutException() } return updatedLayout } + private fun outOfBounds(movedItem: LayoutItem) = + movedItem.x < 0 || movedItem.y < 0 || movedItem.right >= cols || movedItem.bottom >= rows + fun removeItem(id: String): Layout = Layout(items.filter { it.i != id }, cols, rows) @@ -298,155 +204,29 @@ data class Layout( * @param {LayoutItem} collidesWith Layout item we're colliding with. * @param {LayoutItem} itemToMove Layout item we're moving. */ - private fun moveElementAwayFromCollision( + private fun pushCollidingElement( collidesWith: LayoutItem, itemToMove: LayoutItem, isDirectUserAction: Boolean, - compactType: CompactType + direction: Direction ): Layout { - return try { - moveElementInternal( - itemToMove, - if (compactType.isHorizontal) itemToMove.x + 1 else null, - if (compactType.isVertical) itemToMove.y + 1 else null, - false, - collidesWith.isStatic, // we're already colliding (not for static items) - compactType - ) - } catch (e: OutOfBoundsException) { - // If there is enough space above the collision to put this element, move it there. - // We only do this on the main collision as this can get funky in cascades and cause - // unwanted swapping behavior. - if (isDirectUserAction) { - tryMovingUp(collidesWith, itemToMove, compactType) - } else throw e - } - } - - private fun tryMovingUp( - collidesWith: LayoutItem, - itemToMove: LayoutItem, - compactType: CompactType - ): Layout { - // Make a mock item so we don't modify the item here, only modify in moveElement. - val fakeItem = LayoutItem( - if (compactType.isHorizontal) maxOf(collidesWith.x - itemToMove.w, 0) else itemToMove.x, - if (compactType.isVertical) maxOf(collidesWith.y - itemToMove.h, 0) else itemToMove.y, - itemToMove.w, - itemToMove.h, - i = "-1" - ) - - // No collision? If so, we can go up there; otherwise, we'll end up moving down as normal - return if (!anyCollisions(fakeItem)) { - logger.debug { - "Doing reverse collision on ${itemToMove.i} up to [${fakeItem.x},${fakeItem.y}]." - } - moveElementInternal( - itemToMove, - if (compactType.isHorizontal) fakeItem.x else null, - if (compactType.isVertical) fakeItem.y else null, - false, // Reset isUserAction flag because we're not in the main collision anymore. - collidesWith.isStatic, // we're already colliding (not for static items) - compactType - ) - } else this - } - - /** - * Compact an item in the layout. - * - * Returns modified item. - * - */ - private fun compactItem( - compareWith: Layout, - l: LayoutItem, - compactType: CompactType, - cols: Int, - fullLayout: Layout - ): LayoutItem { - var newItem = l - if (compactType.isVertical) { - // Bottom 'y' possible is the bottom of the layout. - // This allows you to do nice stuff like specify {y: Infinity} - // This is here because the layout must be sorted in order to get the correct bottom `y`. - newItem = newItem.copy(y = min(compareWith.bottom(), newItem.y)) - // Move the element up as far as it can go without colliding. - while (newItem.y > 0 && !compareWith.anyCollisions(newItem)) { - newItem = newItem.copy(y = newItem.y - 1) - } - } else if (compactType.isHorizontal) { - // Move the element left as far as it can go without colliding. - while (newItem.x > 0 && !compareWith.anyCollisions(newItem)) { - newItem = newItem.copy(x = newItem.x - 1) - } - } - - // Move it down, and keep moving it down if it's colliding. - var collides: LayoutItem? = compareWith.getFirstCollision(newItem) - while (collides != null) { - newItem = if (compactType.isHorizontal) { - fullLayout.resolveCompactionCollision(newItem, collides.x + collides.w, Axis.x) - } else if (compactType.isVertical) { - fullLayout.resolveCompactionCollision(newItem, collides.y + collides.h, Axis.y) - } else newItem - // Since we can't grow without bounds horizontally, if we've overflown, let's move it down and try again. - if (compactType.isHorizontal && newItem.x + newItem.w > cols) { - newItem = newItem.copy( - x = cols - newItem.w, - y = newItem.y + 1 - ) - } + // Don't move static items - we have to move the other element away. + if (itemToMove.isStatic) { + if (collidesWith.isStatic) throw ImpossibleLayoutException() - collides = compareWith.getFirstCollision(newItem) + return pushCollidingElement(itemToMove, collidesWith, isDirectUserAction, direction) } - // Ensure that there are no negative positions - return newItem.copy( - x = max(newItem.x, 0), - y = max(newItem.y, 0) + return moveElementInternal( + itemToMove, + itemToMove.x + direction.xIncr, + itemToMove.y + direction.yIncr, + false, + direction + // we're already colliding (not for static items) ) } - /** - * Get layout items sorted from top left to right and down. - * - * @return {Array} Array of layout objects. - * @return {Array} Layout, sorted static items first. - */ - private fun sortLayoutItems(compactType: CompactType): Layout = - when (compactType) { - CompactType.Horizontal -> sortLayoutItemsByColRow() - CompactType.Vertical -> sortLayoutItemsByRowCol() - else -> this - } - - /** - * Sort layout items by row ascending and column ascending. - * - * Does not modify Layout. - */ - private fun sortLayoutItemsByRowCol(): Layout = - Layout(items.sortedWith { a, b -> - if (a.y > b.y || (a.y == b.y && a.x > b.x)) { - 1 - } else if (a.y == b.y && a.x == b.x) { - // Without this, we can get different sort results in IE vs. Chrome/FF - 0 - } else -1 - }, cols, rows) - - /** - * Sort layout items by column ascending then row ascending. - * - * Does not modify Layout. - */ - private fun sortLayoutItemsByColRow(): Layout = - Layout(items.sortedWith { a, b -> - if (a.x > b.x || (a.x == b.x && a.y > b.y)) 1 else -1 - }, cols, rows) - fun canonicalize(): Layout = Layout(items.sortedWith { a, b -> if (a.y > b.y || @@ -455,14 +235,8 @@ data class Layout( ) 1 else -1 }, cols, rows) - private class OutOfBoundsException : Exception() companion object { private val logger = Logger() - - private val heightWidth = mapOf( - Axis.x to { layoutItem: LayoutItem -> layoutItem.w }, - Axis.y to { layoutItem: LayoutItem -> layoutItem.h } - ) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/baaahs/ui/gridlayout/types.kt b/src/commonMain/kotlin/baaahs/ui/gridlayout/types.kt index b858013fc5..ba053d4bed 100644 --- a/src/commonMain/kotlin/baaahs/ui/gridlayout/types.kt +++ b/src/commonMain/kotlin/baaahs/ui/gridlayout/types.kt @@ -41,6 +41,32 @@ data class LayoutItem( } } +/** + * Sort layout items by row ascending then column ascending. + */ +fun List.sortLayoutItemsByRowCol(reverse: Boolean = false): List = + sortedWith { a, b -> + when { + a.y == b.y && a.x == b.x -> 0 + a.y == b.y && a.x > b.x -> 1 + if (reverse) b.y > a.y else a.y > b.y -> 1 + else -> -1 + } + } + +/** + * Sort layout items by column ascending then row ascending. + */ +fun List.sortLayoutItemsByColRow(reverse: Boolean = false): List = + sortedWith { a, b -> + when { + a.x == b.x && a.y == b.y -> 0 + a.x == b.x && a.y > b.y -> 1 + if (reverse) a.x > b.x else b.x > a.x -> 1 + else -> -1 + } + } + data class LayoutItemSize( val width: Int, val height: Int @@ -53,31 +79,57 @@ data class Position( val height: Int ) -enum class CompactType { - Horizontal, - Vertical, - None; +enum class Direction( + val xIncr: Int, val yIncr: Int +) { + North(0, -1) { + override val opposite: Direction get() = South - val isHorizontal get() = this == Horizontal - val isVertical get() = this == Vertical + override fun sort(collisions: List): List = + collisions.sortLayoutItemsByRowCol(reverse = true) + }, - companion object { - fun LayoutItem.determineFrom(newX: Int, newY: Int) = - if (newX != x) Horizontal else Vertical - } -} + South(0, 1) { + override val opposite: Direction get() = North -enum class Axis { - x { - override fun incr(item: LayoutItem) = item.copy(x = item.x + 1) - override fun set(item: LayoutItem, value: Int): LayoutItem = item.copy(x = value) + override fun sort(collisions: List): List = + collisions.sortLayoutItemsByRowCol() }, - y { - override fun incr(item: LayoutItem) = item.copy(y = item.y + 1) - override fun set(item: LayoutItem, value: Int): LayoutItem = item.copy(y = value) + + East(1, 0) { + override val opposite: Direction get() = West + + override fun sort(collisions: List): List = + collisions.sortLayoutItemsByRowCol() + + }, + + West(-1, 0) { + override val opposite: Direction get() = East + + override fun sort(collisions: List): List = + collisions.sortLayoutItemsByRowCol(reverse = true) + }; - abstract fun incr(item: LayoutItem): LayoutItem - abstract fun set(item: LayoutItem, value: Int): LayoutItem + val isHorizontal get() = xIncr != 0 + val isVertical get() = yIncr != 0 + abstract val opposite: Direction + + abstract fun sort(collisions: List): List + + companion object { + fun rankedPushOptions(x: Int, y: Int): Array { + return when { + x > 0 && y == 0 -> arrayOf(West, East, South, North) + x < 0 && y == 0 -> arrayOf(East, West, South, North) + x == 0 && y > 0 -> arrayOf(North, South, East, West) + x == 0 && y < 0 -> arrayOf(South, North, East, West) + else -> arrayOf(South, North, East, West) + } + } + } } + +class ImpossibleLayoutException : Exception() diff --git a/src/commonTest/kotlin/baaahs/ui/gridlayout/GridSpec.kt b/src/commonTest/kotlin/baaahs/ui/gridlayout/GridSpec.kt index ab1bcee8c6..86bcabdfe5 100644 --- a/src/commonTest/kotlin/baaahs/ui/gridlayout/GridSpec.kt +++ b/src/commonTest/kotlin/baaahs/ui/gridlayout/GridSpec.kt @@ -74,13 +74,17 @@ fun String.toLayout(): Layout { fun Layout.stringify(): String { if (items.isEmpty()) return "[Empty]" - val gridWidth = items.maxOf { it.x + it.w } - val gridHeight = items.maxOf { it.y + it.h } + val gridWidth = if (cols == Int.MAX_VALUE) items.maxOf { it.x + it.w } else cols + val gridHeight = if (rows == Int.MAX_VALUE) items.maxOf { it.y + it.h } else rows val cells = Array(gridHeight) { Array(gridWidth) { "." } } items.forEach { (0 until it.h).forEach { row -> (0 until it.w).forEach { col -> - cells[it.y + row][it.x + col] = it.i + if (cells[it.y + row][it.x + col] != ".") { + cells[it.y + row][it.x + col] = "!" + } else { + cells[it.y + row][it.x + col] = it.i + } } } } diff --git a/src/commonTest/kotlin/baaahs/ui/gridlayout/LayoutSpec.kt b/src/commonTest/kotlin/baaahs/ui/gridlayout/LayoutSpec.kt index 655090f721..1c9a74d788 100644 --- a/src/commonTest/kotlin/baaahs/ui/gridlayout/LayoutSpec.kt +++ b/src/commonTest/kotlin/baaahs/ui/gridlayout/LayoutSpec.kt @@ -1,8 +1,9 @@ package baaahs.ui.gridlayout import baaahs.describe +import baaahs.gl.override import baaahs.toEqual -import baaahs.ui.gridlayout.CompactType.Companion.determineFrom +import ch.tutteli.atrium.api.fluent.en_GB.toThrow import ch.tutteli.atrium.api.verbs.expect import org.spekframework.spek2.Spek @@ -19,49 +20,88 @@ object LayoutSpec : Spek({ val move by value { { id: String, x: Int, y: Int -> val item = layout.find(id)!! - val compactType = item.determineFrom(x, y) - layout.moveElement(item, x, y, false, compactType) + layout.moveElement(item, item.x + x, item.y + y) + .stringify() } } - it("moving A one space down shifts D down") { - expect(move("A", 0, 1).stringify()).toEqual(""" - .BC. + it("moving A one space down swaps A and D") { + expect(move("A", 0, 1)).toEqual(""" + DBC. AEFG - DHI. + .HI. """.trimIndent()) } + it("moving C to E's spot fails") { + expect { (move("C", -1, -1)) }.toThrow() + } + it("moving A two spaces down leaves the rest undisturbed") { - expect(move("A", 0, 2).stringify()).toEqual(""" + expect(move("A", 0, 2)).toEqual(""" .BC. DEFG AHI. """.trimIndent()) } - it("moving A one space right shifts B and C over, because there's room to the right") { - expect(move("A", 1, 0).stringify()).toEqual(""" - .ABC + it("moving A one space right swaps A and B") { + expect(move("A", 1, 0)).toEqual(""" + BAC. DEFG .HI. """.trimIndent()) } it("moving A two spaces right shifts C over") { - expect(move("A", 2, 0).stringify()).toEqual(""" - .BAC + expect(move("A", 2, 0)).toEqual(""" + BCA. DEFG .HI. """.trimIndent()) } it("moving D one space right shifts E into its place") { - expect(move("D", 1, 1).stringify()).toEqual(""" + expect(move("D", 1, 0)).toEqual(""" ABC. EDFG .HI. """.trimIndent()) } + + it("moving B one space down and left shifts E down") { + expect(move("B", -1, 1)).toEqual(""" + A.C. + BEFG + DHI. + """.trimIndent()) + } + + context("with ABCDEF in one row") { + override(layout) { "ABCDEF".toLayout() } + it("moving B two spaces over") { + expect(move("B", 2, 0)).toEqual("ACDBEF") + } + } + + context("with .ABBC.") { + override(layout) { ".ABBC.".toLayout() } + + xit("moving A one space right should swap A and B") { + expect(move("A", 1, 0)).toEqual(".BBAC.") + } + + it("moving A two spaces right should swap A and B") { + expect(move("A", 2, 0)).toEqual(".BBAC.") + } + + xit("moving C one space left should swap B and C") { + expect(move("C", -1, 0)).toEqual(".ACBB.") + } + + it("moving C two spaces left should swap B and C") { + expect(move("C", -2, 0)).toEqual(".ACBB.") + } + } } }) \ No newline at end of file diff --git a/src/jsMain/kotlin/baaahs/app/ui/ControlsPalette.kt b/src/jsMain/kotlin/baaahs/app/ui/ControlsPalette.kt index 2482fcc2ee..c52fbdafe9 100644 --- a/src/jsMain/kotlin/baaahs/app/ui/ControlsPalette.kt +++ b/src/jsMain/kotlin/baaahs/app/ui/ControlsPalette.kt @@ -8,7 +8,6 @@ import baaahs.show.live.ControlProps import baaahs.show.live.LegacyControlDisplay import baaahs.show.live.OpenShow import baaahs.ui.* -import baaahs.ui.gridlayout.CompactType import baaahs.ui.gridlayout.Layout import baaahs.ui.gridlayout.LayoutItem import baaahs.ui.gridlayout.gridLayout @@ -101,7 +100,6 @@ val ControlsPalette = xComponent("ControlsPalette") { prop attrs.margin = 5 to 5 attrs.layout = layout // attrs.onLayoutChange = handleLayoutChange - attrs.compactType = CompactType.None attrs.disableDrag = !editMode.isOn attrs.disableResize = true attrs.isDroppable = editMode.isOn diff --git a/src/jsMain/kotlin/baaahs/app/ui/controls/GridButtonGroupControlView.kt b/src/jsMain/kotlin/baaahs/app/ui/controls/GridButtonGroupControlView.kt index bfcaa68f7c..a2597429c4 100644 --- a/src/jsMain/kotlin/baaahs/app/ui/controls/GridButtonGroupControlView.kt +++ b/src/jsMain/kotlin/baaahs/app/ui/controls/GridButtonGroupControlView.kt @@ -182,7 +182,6 @@ private val GridButtonGroupControlView = xComponent("GridB attrs.margin = 5 to 5 attrs.layout = layout attrs.onLayoutChange = handleLayoutChange - attrs.compactType = CompactType.None attrs.resizeHandle = ::buildResizeHandle attrs.disableDrag = !editMode.isOn attrs.disableResize = !editMode.isOn diff --git a/src/jsMain/kotlin/baaahs/app/ui/layout/GridTabLayoutView.kt b/src/jsMain/kotlin/baaahs/app/ui/layout/GridTabLayoutView.kt index 65c8efcc8c..7d5c89a152 100644 --- a/src/jsMain/kotlin/baaahs/app/ui/layout/GridTabLayoutView.kt +++ b/src/jsMain/kotlin/baaahs/app/ui/layout/GridTabLayoutView.kt @@ -160,7 +160,6 @@ private val GridTabLayoutView = xComponent("GridTabLayout") attrs.margin = 5 to 5 attrs.layout = layoutGrid.layout attrs.onLayoutChange = handleLayoutChange - attrs.compactType = CompactType.None attrs.resizeHandle = ::buildResizeHandle attrs.disableDrag = !editMode.isOn attrs.disableResize = !editMode.isOn diff --git a/src/jsMain/kotlin/baaahs/app/ui/layout/LayoutStyles.kt b/src/jsMain/kotlin/baaahs/app/ui/layout/LayoutStyles.kt index 0250b12666..688990bcc0 100644 --- a/src/jsMain/kotlin/baaahs/app/ui/layout/LayoutStyles.kt +++ b/src/jsMain/kotlin/baaahs/app/ui/layout/LayoutStyles.kt @@ -81,6 +81,7 @@ class LayoutStyles(val theme: Theme) : StyleSheet("app-ui-layout", isStatic = tr border(3.px, BorderStyle.solid, theme.palette.text.primary.asColor().withAlpha(.25)) transition(::opacity, transitionTime) transition(::border, transitionTime) + cursor = Cursor.default // outlineWidth = 3.px // put("outlineStyle", "dashed") @@ -158,6 +159,7 @@ class LayoutStyles(val theme: Theme) : StyleSheet("app-ui-layout", isStatic = tr bottom = (-2).px + (2.5).em zIndex = StyleConstants.Layers.aboveSharedGlCanvas filter = "drop-shadow(1px 1px 2px rgba(0, 0, 0, .9))" + cursor = Cursor.default child("svg") { width = .75.em @@ -170,6 +172,7 @@ class LayoutStyles(val theme: Theme) : StyleSheet("app-ui-layout", isStatic = tr bottom = (-2).px + 1.em zIndex = StyleConstants.Layers.aboveSharedGlCanvas filter = "drop-shadow(1px 1px 2px rgba(0, 0, 0, .9))" + cursor = Cursor.default child("svg") { width = .75.em @@ -290,6 +293,10 @@ class LayoutStyles(val theme: Theme) : StyleSheet("app-ui-layout", isStatic = tr borderBottom = "2px solid rgba(0, 0, 0, 0.4)" } + ".react-draggable > .app-ui-controls-controlRoot" { + pointerEvents = PointerEvents.none + } + ".react-draggable-dragging, .react-draggable-dragging *" { pointerEvents = PointerEvents.none transition(::top, 0.s) @@ -298,6 +305,18 @@ class LayoutStyles(val theme: Theme) : StyleSheet("app-ui-layout", isStatic = tr transition(::height, 0.s) } + ".react-draggable" { + cursor = Cursor.grab + } + + ".react-draggable-dragging" { + cursor = Cursor.grabbing + } + + ".react-draggable-not-droppable-here" { + cursor = Cursor.noDrop + } + val inset = 2.px ".react-resizable-hide > .app-ui-layout-resize-handle" { diff --git a/src/jsMain/kotlin/baaahs/ui/gridlayout/GridItem.kt b/src/jsMain/kotlin/baaahs/ui/gridlayout/GridItem.kt index c00bf36859..3fe78ddc3f 100644 --- a/src/jsMain/kotlin/baaahs/ui/gridlayout/GridItem.kt +++ b/src/jsMain/kotlin/baaahs/ui/gridlayout/GridItem.kt @@ -43,6 +43,7 @@ external interface GridItemProps : Props { var usePercentages: Boolean? var transformScale: Double var droppingPosition: DroppingPosition? + var notDroppableHere: Boolean? var className: String var style: Any? @@ -611,6 +612,7 @@ class GridItem( this["react-draggable-dragging"] = state.dragging this.dropping = droppingPosition this.cssTransforms = useCSSTransforms + this["react-draggable-not-droppable-here"] = props.notDroppableHere == true } ) // We can set the width and height on the child, but unfortunately we can't set the position. diff --git a/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayout.kt b/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayout.kt index d16571ed8a..19d82cf323 100644 --- a/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayout.kt +++ b/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayout.kt @@ -4,8 +4,6 @@ import baaahs.app.ui.layout.DragNDropContext import baaahs.app.ui.layout.GridLayoutContext import baaahs.app.ui.layout.dragNDropContext import baaahs.geom.Vector2D -import baaahs.replace -import baaahs.ui.gridlayout.CompactType.Companion.determineFrom import baaahs.window import baaahs.y import external.lodash.isEqual @@ -19,7 +17,6 @@ import react.dom.* import react.dom.events.DragEvent import react.dom.events.DragEventHandler import styled.inlineStyles -import kotlin.math.min import kotlin.math.roundToInt import kotlin.reflect.KClass @@ -36,10 +33,10 @@ external interface GridLayoutState : State { var oldResizeItem: LayoutItem? var droppingDOMNode: ReactElement<*>? var droppingPosition: DroppingPosition? + var notDroppableHere: Boolean? // Mirrored props var children: ReactNode? - var compactType: CompactType? var propsLayout: Layout? } @@ -58,10 +55,8 @@ class GridLayout( props.layout ?: Layout(emptyList(), props.cols!!, props.maxRows), props.children?.asArray() ?: emptyArray(), props.cols!!, - props.maxRows, + props.maxRows // Legacy support for verticalCompact: false - CompactType.None, - props.allowOverlap ) mounted = false oldDragItem = null @@ -100,6 +95,7 @@ class GridLayout( !isEqual(props, nextProps) || // !fastRGLPropsEqual(propsD, nextProps, ::isEqual) || state.draggingPlaceholder != nextState.draggingPlaceholder || + state.notDroppableHere != nextState.notDroppableHere || state.mounted != nextState.mounted || state.droppingPosition != nextState.droppingPosition ) @@ -191,23 +187,30 @@ class GridLayout( * @param {Element} node The current dragging DOM element */ fun onDragItem(i: String, x: Int, y: Int, gridDragEvent: GridDragEvent) { + state.draggingPlaceholder?.let { placeholder -> + if (placeholder.i == i && placeholder.x == x && placeholder.y == y) { + return + } + } + val e = gridDragEvent.e val node = gridDragEvent.node val oldDragItem = state.oldDragItem - val allowOverlap = props.allowOverlap == true - val preventCollision = props.preventCollision!! val oldLayout = state.oldLayout!! val l = oldLayout.find(i) ?: return run { console.log("GridLayout(${props.id}),onDragItem() couldn't find item $i") } - val compactType = l.determineFrom(x, y) - // Move the element to the dragged location. - val newLayout = oldLayout.moveElement( - l, x, y, preventCollision, compactType, allowOverlap - ) + val newLayout = try { + oldLayout.moveElement(l, x, y) + } catch (e: ImpossibleLayoutException) { + setState { + this.notDroppableHere = true + } + return + } val newItem = newLayout.find(i) ?: return // Create placeholder (display only) @@ -225,8 +228,9 @@ class GridLayout( props.onDrag(newLayout, oldDragItem, newItem, placeholder, e, node) setState { - this.layout = if (allowOverlap) newLayout else newLayout.compact(CompactType.None) + this.layout = newLayout.resetMovedFlag() this.draggingPlaceholder = placeholder + this.notDroppableHere = null } } @@ -249,26 +253,21 @@ class GridLayout( console.log("GridLayout", props.id, "onDragStop", i, state.draggingPlaceholder) if (state.draggingPlaceholder == null) return val oldDragItem = state.oldDragItem - val allowOverlap = props.allowOverlap == true - val preventCollision = props.preventCollision!! val l = oldLayout.find(i) ?: return run { console.log("GridLayout(${props.id}),onDragStop() couldn't find layout $i") } - val compactType = l.determineFrom(x, y) - // Move the element here - var newLayout = oldLayout.moveElement( - l, x, y, preventCollision, compactType, allowOverlap - ) + var newLayout = oldLayout.moveElement(l, x, y) val newItem = newLayout.find(i) ?: return props.onDragStop(newLayout, oldDragItem, newItem, null, e, node) - newLayout = if (allowOverlap) newLayout else newLayout.compact(CompactType.None) + newLayout = newLayout.resetMovedFlag() setState { this.draggingPlaceholder = null + this.notDroppableHere = null this.layout = newLayout this.oldDragItem = null this.oldLayout = null @@ -310,44 +309,8 @@ class GridLayout( val node = gridResizeEvent.node val layout = state.layout val oldResizeItem = state.oldResizeItem - val allowOverlap = props.allowOverlap!! - val preventCollision = props.preventCollision!! - - val newListItems = ArrayList(layout.items) - newListItems.replace({ it.i == i }) { l -> - var newItem = l - // Something like quad tree should be used - // to find collisions faster - val hasCollisions: Boolean - if (preventCollision && !allowOverlap) { - val collisions = layout.getAllCollisions(newItem.copy(w = w, h = h)) - .filter { layoutItem -> layoutItem.i != newItem.i } - hasCollisions = collisions.isNotEmpty() - - // If we're colliding, we need adjust the placeholder. - if (hasCollisions) { - // adjust w && h to maximum allowed space - var leastX = Int.MAX_VALUE - var leastY = Int.MAX_VALUE - collisions.forEach { layoutItem -> - if (layoutItem.x > newItem.x) leastX = min(leastX, layoutItem.x) - if (layoutItem.y > newItem.y) leastY = min(leastY, layoutItem.y) - } - if (leastX != Int.MAX_VALUE) newItem = newItem.copy(w = leastX - newItem.x) - if (leastY != Int.MAX_VALUE) newItem = newItem.copy(h = leastY - newItem.y) - } - } else hasCollisions = false - - if (!hasCollisions) { - // Set new width and height. - newItem = newItem.copy(w = w, h = h) - } - - newItem - } - - val newLayout = Layout(newListItems, layout.cols, layout.rows) + val newLayout = layout.resizeElement(layout.find(i)!!, w, h) val l = newLayout.find(i) ?: return @@ -357,11 +320,9 @@ class GridLayout( props.onResize(newLayout, oldResizeItem, l, placeholder, e, node) - val compactType = CompactType.None - // Re-compact the newLayout and set the drag placeholder. setState { - this.layout = if (allowOverlap) newLayout else newLayout.compact(CompactType.None) + this.layout = newLayout.resetMovedFlag() this.draggingPlaceholder = placeholder } } @@ -374,13 +335,12 @@ class GridLayout( val node = gridResizeEvent.node val layout = state.layout val oldResizeItem = state.oldResizeItem - val allowOverlap = props.allowOverlap!! val l = layout.find(i)!! props.onResizeStop(layout, oldResizeItem, l, null, e, node) // Set state - val newLayout = if (allowOverlap) layout else layout.compact(CompactType.None) + val newLayout = layout.resetMovedFlag() val oldLayout = state.oldLayout setState { this.draggingPlaceholder = null @@ -487,6 +447,9 @@ class GridLayout( attrs.useCSSTransforms = useCSSTransforms && mounted attrs.usePercentages = !mounted attrs.transformScale = transformScale + if (state.notDroppableHere == true && state.originalDragItem?.i == l.i) { + attrs.notDroppableHere = true + } attrs.w = l.w attrs.h = l.h attrs.x = l.x @@ -600,12 +563,13 @@ class GridLayout( val layout = state.layout val newLayout = Layout(layout.items.filter { l -> l.i != droppingItem?.i }, layout.cols, layout.rows) - .compact(CompactType.None) + .resetMovedFlag() setState { this.layout = newLayout this.droppingDOMNode = null this.draggingPlaceholder = null + this.notDroppableHere = null this.droppingPosition = undefined } } @@ -698,7 +662,7 @@ class GridLayout( } companion object : RStatics>(GridLayout::class) { - const val debug = true + const val debug = false init { displayName = GridLayout::class.simpleName @@ -725,7 +689,6 @@ class GridLayout( useCSSTransforms = true transformScale = 1.0 // verticalCompact = true - compactType = CompactType.Vertical preventCollision = false droppingItem = DroppingItem( "__dropping-elem__", @@ -754,8 +717,7 @@ class GridLayout( // Legacy support for compactType // Allow parent to set layout directly. if ( - nextProps.layout != prevState.propsLayout || - nextProps.compactType != prevState.compactType + nextProps.layout != prevState.propsLayout ) { baseLayout = nextProps.layout } else if (!childrenEqual( @@ -775,16 +737,13 @@ class GridLayout( baseLayout, nextProps.children?.asArray() ?: emptyArray(), nextProps.cols!!, - nextProps.maxRows, - CompactType.None, - nextProps.allowOverlap + nextProps.maxRows ) jso { this.layout = newLayout // We need to save these props to state for using // getDerivedStateFromProps instead of componentDidMount (in which we would get extra rerender) - this.compactType = nextProps.compactType this.children = nextProps.children this.propsLayout = nextProps.layout } @@ -797,9 +756,7 @@ class GridLayout( initialLayout: Layout, children: ReactChildren, cols: Int, - rows: Int, - compactType: CompactType, - allowOverlap: Boolean? + rows: Int ): Layout { // Generate one layout item per child. val layoutItems = mutableListOf() @@ -828,10 +785,7 @@ class GridLayout( // Correct the layout. val correctedLayout = Layout(layoutItems, cols, rows).correctBounds() - return if (allowOverlap == true) - correctedLayout - else - correctedLayout.compact(CompactType.None) + return correctedLayout.resetMovedFlag() } private val noop: DragEventHandler<*> = { } diff --git a/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayoutProps.kt b/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayoutProps.kt index dc1e0832fe..77dc7367b0 100644 --- a/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayoutProps.kt +++ b/src/jsMain/kotlin/baaahs/ui/gridlayout/GridLayoutProps.kt @@ -50,7 +50,7 @@ external interface GridLayoutProps : PropsWithChildren { var draggableHandle: String? // = '' // Compaction type. - var compactType: CompactType? // ?('vertical' | 'horizontal') = 'vertical'; +// var compactType: CompactType? // ?('vertical' | 'horizontal') = 'vertical'; /** * Layout is an array of object with the format: