Skip to content

Commit

Permalink
Table
Browse files Browse the repository at this point in the history
  • Loading branch information
devkanro committed Aug 29, 2024
1 parent c8ec545 commit c4fcde1
Show file tree
Hide file tree
Showing 75 changed files with 6,293 additions and 45 deletions.
1 change: 1 addition & 0 deletions foundation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ private val composeVersion

dependencies {
api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")
implementation("org.jetbrains.compose.ui:ui-util-desktop:$composeVersion")

testImplementation(compose.desktop.uiTestJUnit4)
testImplementation(compose.desktop.currentOs) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.jetbrains.jewel.foundation

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.Constraints
import org.jetbrains.jewel.foundation.modifier.onHover

@ExperimentalJewelApi
@Composable
public fun OverflowBox(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
overflowZIndex: Float = 1f,
content: @Composable BoxScope.() -> Unit,
) {
var showOverflow by remember { mutableStateOf(false) }

Box(
Modifier
.layout { measurable, constraints ->
val predictWidth = measurable.maxIntrinsicWidth(constraints.maxHeight)
val constraintWith = constraints.maxWidth

val overflowing = showOverflow && predictWidth > constraintWith

val targetConstraints =
if (overflowing) {
constraints.copy(
minWidth = 0,
maxWidth = Constraints.Infinity,
)
} else {
constraints
}
val zIndex = if (overflowing) overflowZIndex else 0f

// Trick for overflow layout, I don't know why cell align center without offset.
val offset = if (overflowing) (predictWidth - constraintWith) / 2 else 0

val placements = measurable.measure(targetConstraints)
layout(placements.width, placements.height) {
placements.placeRelative(offset, 0, zIndex)
}
}.onHover {
showOverflow = it
}.then(modifier),
contentAlignment = contentAlignment,
content = content,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.jetbrains.jewel.foundation.lazy.draggable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.modifier.ModifierLocal
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.zIndex

internal val ModifierLocalDraggableLayoutOffset = modifierLocalOf { Offset.Zero }

internal fun Modifier.draggableLayout(): Modifier =
composed {
var offset by remember { mutableStateOf(Offset.Zero) }

this
.onGloballyPositioned {
offset = it.positionInRoot()
}.modifierLocalProvider(ModifierLocalDraggableLayoutOffset) {
offset
}
}

internal fun Modifier.draggingGestures(
stateLocal: ModifierLocal<LazyLayoutDraggingState<*>>,
key: Any?,
): Modifier =
composed {
var state by remember { mutableStateOf<LazyLayoutDraggingState<*>?>(null) }
var itemOffset by remember { mutableStateOf(Offset.Zero) }
var layoutOffset by remember { mutableStateOf(Offset.Zero) }

modifierLocalConsumer {
state = stateLocal.current
layoutOffset = ModifierLocalDraggableLayoutOffset.current
}.then(
if (state != null) {
Modifier
.onGloballyPositioned {
itemOffset = it.positionInRoot()
}.pointerInput(Unit) {
detectDragGestures(onDrag = { change, offset ->
change.consume()
state?.onDrag(offset)
}, onDragStart = {
state?.onDragStart(key, it + itemOffset - layoutOffset)
}, onDragEnd = {
state?.onDragInterrupted()
}, onDragCancel = {
state?.onDragInterrupted()
})
}
} else {
Modifier
},
)
}

internal fun Modifier.draggingOffset(
stateLocal: ModifierLocal<LazyLayoutDraggingState<*>>,
key: Any?,
orientation: Orientation? = null,
): Modifier =
composed {
var state by remember { mutableStateOf<LazyLayoutDraggingState<*>?>(null) }
val dragging = state?.draggingItemKey == key

this
.modifierLocalConsumer {
state = stateLocal.current
}.then(
if (state != null && dragging) {
Modifier.zIndex(2f).graphicsLayer(
translationX =
if (orientation == Orientation.Vertical) {
0f
} else {
state?.draggingItemOffsetTransformX
?: 0f
},
translationY =
if (orientation == Orientation.Horizontal) {
0f
} else {
state?.draggingItemOffsetTransformY
?: 0f
},
)
} else {
Modifier
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.jetbrains.jewel.foundation.lazy.draggable

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size

public abstract class LazyLayoutDraggingState<T> {
public var draggingItemOffsetTransformX: Float by mutableStateOf(0f)

public var draggingItemOffsetTransformY: Float by mutableStateOf(0f)

public var draggingItemKey: Any? by mutableStateOf(null)

public var initialOffset: Offset = Offset.Zero

public var draggingOffset: Offset = Offset.Zero

internal val interactionSource: MutableInteractionSource = MutableInteractionSource()

public fun onDragStart(
key: Any?,
offset: Offset,
) {
draggingItemKey = key
initialOffset = offset
draggingOffset = Offset.Zero
draggingItemOffsetTransformX = 0f
draggingItemOffsetTransformY = 0f

println("Drag start with '$key' at '$offset'")
}

public fun onDrag(offset: Offset) {
draggingItemOffsetTransformX += offset.x
draggingItemOffsetTransformY += offset.y
draggingOffset += offset

val draggingItem = getItemWithKey(draggingItemKey ?: return) ?: return
val hoverItem = getReplacingItem(draggingItem)

if (hoverItem != null && draggingItem.key != hoverItem.key) {
val targetOffset =
if (draggingItem.index < hoverItem.index) {
val maxOffset = hoverItem.offset + Offset(hoverItem.size.width, hoverItem.size.height)
maxOffset - Offset(draggingItem.size.width, draggingItem.size.height)
} else {
hoverItem.offset
}

val changedOffset = draggingItem.offset - targetOffset

println("Drag '${draggingItem.key}(${draggingItem.size})' at ${draggingItem.offset}")
println("Over '${hoverItem.key}(${hoverItem.size})' at ${hoverItem.offset}")
println("Into $targetOffset With $changedOffset")

if (moveItem(draggingItem.key, hoverItem.key)) {
draggingItemOffsetTransformX += changedOffset.x
draggingItemOffsetTransformY += changedOffset.y
}
}
}

public fun onDragInterrupted() {
println("Drag end")

draggingItemKey = null
initialOffset = Offset.Zero
draggingOffset = Offset.Zero
draggingItemOffsetTransformX = 0f
draggingItemOffsetTransformY = 0f
}

public abstract fun canMove(key: Any?): Boolean

public abstract fun moveItem(
from: Any?,
to: Any?,
): Boolean

public abstract fun getReplacingItem(draggingItem: T): T?

public abstract fun getItemWithKey(key: Any): T?

public abstract val T.offset: Offset

public abstract val T.size: Size

public abstract val T.index: Int

public abstract val T.key: Any?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.jetbrains.jewel.foundation.lazy.selectable

public interface SelectionEvent
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.jetbrains.jewel.foundation.lazy.selectable

import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.input.pointer.isCtrlPressed
import androidx.compose.ui.input.pointer.isMetaPressed
import androidx.compose.ui.input.pointer.isShiftPressed
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalModifierNode
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.node.DelegatingNode

public interface SelectionManager {
public val interactionSource: MutableInteractionSource

public fun isSelectable(itemKey: Any?): Boolean

public fun isSelected(itemKey: Any?): Boolean

public fun handleEvent(event: SelectionEvent)

public fun clearSelection()
}

internal val ModifierLocalSelectionManager = modifierLocalOf<SelectionManager?> { null }

public fun Modifier.selectionManager(manager: SelectionManager): Modifier =
focusable(interactionSource = manager.interactionSource)
.focusGroup()
.modifierLocalProvider(ModifierLocalSelectionManager) {
manager
}

public fun Modifier.selectionManagerConsumer(factory: @Composable (SelectionManager) -> Modifier): Modifier =
composed {
var manager by remember { mutableStateOf<SelectionManager?>(null) }

this
.modifierLocalConsumer {
manager = ModifierLocalSelectionManager.current
}.then(if (manager != null) factory(manager!!) else Modifier)
}

internal fun PointerKeyboardModifiers.selectionType(): SelectionType =
when {
this.isCtrlPressed || this.isMetaPressed -> SelectionType.Multi
this.isShiftPressed -> SelectionType.Contiguous
else -> SelectionType.Normal
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.jetbrains.jewel.foundation.lazy.selectable

/**
* Specifies the selection mode for a selectable lazy list.
*/
public enum class SelectionMode {
/**
* No selection is allowed.
*/
None,

/**
* Only a single cell can be selected.
*/
Single,

/**
* Multiple cells can be selected.
*/
Multiple,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.jetbrains.jewel.foundation.lazy.selectable

public enum class SelectionType {
Normal,
Contiguous,
Multi,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.jetbrains.jewel.foundation.lazy.table

import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.OnGloballyPositionedModifier
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

internal class AwaitFirstLayoutModifier : OnGloballyPositionedModifier {

private var wasPositioned = false
private var continuation: Continuation<Unit>? = null

suspend fun waitForFirstLayout() {
if (!wasPositioned) {
val oldContinuation = continuation
suspendCoroutine { continuation = it }
oldContinuation?.resume(Unit)
}
}

override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
if (!wasPositioned) {
wasPositioned = true
continuation?.resume(Unit)
continuation = null
}
}
}
Loading

0 comments on commit c4fcde1

Please sign in to comment.