-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
40 changed files
with
3,124 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
foundation/src/main/kotlin/org/jetbrains/jewel/foundation/OverflowBox.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
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 | ||
) | ||
} |
29 changes: 29 additions & 0 deletions
29
...ion/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/table/AwaitFirstLayoutModifier.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
248 changes: 248 additions & 0 deletions
248
foundation/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/table/LazyTable.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
package org.jetbrains.jewel.foundation.lazy.table | ||
|
||
import androidx.compose.foundation.gestures.FlingBehavior | ||
import androidx.compose.foundation.gestures.Orientation | ||
import androidx.compose.foundation.gestures.ScrollableDefaults | ||
import androidx.compose.foundation.gestures.scrollable | ||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.PaddingValues | ||
import androidx.compose.foundation.layout.calculateEndPadding | ||
import androidx.compose.foundation.layout.calculateStartPadding | ||
import androidx.compose.foundation.lazy.layout.LazyLayout | ||
import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope | ||
import androidx.compose.foundation.overscroll | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.snapshots.Snapshot | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.graphics.RectangleShape | ||
import androidx.compose.ui.layout.MeasureResult | ||
import androidx.compose.ui.layout.Placeable | ||
import androidx.compose.ui.platform.LocalLayoutDirection | ||
import androidx.compose.ui.unit.Constraints | ||
import androidx.compose.ui.unit.IntOffset | ||
import androidx.compose.ui.unit.IntSize | ||
import androidx.compose.ui.unit.constrainHeight | ||
import androidx.compose.ui.unit.constrainWidth | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.offset | ||
|
||
@Composable | ||
public fun LazyTable( | ||
modifier: Modifier = Modifier, | ||
cellSize: LazyTableCellSize, | ||
state: LazyTableState = rememberLazyTableState(), | ||
pinnedColumns: Int = 0, | ||
pinnedRows: Int = 0, | ||
contentPadding: PaddingValues = PaddingValues(0.dp), | ||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), | ||
userScrollEnabled: Boolean = true, | ||
beyondBoundsItemCount: Int = 0, | ||
horizontalArrangement: Arrangement.Horizontal? = null, | ||
verticalArrangement: Arrangement.Vertical? = null, | ||
content: LazyTableScope.() -> Unit, | ||
) { | ||
val itemProvider = rememberLazyTableItemProvider(state, pinnedColumns, pinnedRows, content) | ||
|
||
val measurePolicy = rememberLazyTabletMeasurePolicy( | ||
itemProvider = itemProvider, | ||
cellSize = cellSize, | ||
state = state, | ||
pinnedColumns = pinnedColumns, | ||
pinnedRows = pinnedRows, | ||
contentPadding = contentPadding, | ||
beyondBoundsItemCount = beyondBoundsItemCount, | ||
horizontalArrangement = horizontalArrangement, | ||
verticalArrangement = verticalArrangement, | ||
) | ||
|
||
val overscrollEffect = ScrollableDefaults.overscrollEffect() | ||
|
||
LazyLayout( | ||
modifier = modifier.then(state.remeasurementModifier) | ||
.then(state.awaitLayoutModifier) | ||
.overscroll(overscrollEffect) | ||
.scrollable( | ||
orientation = Orientation.Vertical, | ||
interactionSource = state.internalInteractionSource, | ||
reverseDirection = ScrollableDefaults.reverseDirection( | ||
LocalLayoutDirection.current, | ||
Orientation.Vertical, | ||
false, | ||
), | ||
flingBehavior = flingBehavior, | ||
state = state.verticalScrollableState, | ||
overscrollEffect = overscrollEffect, | ||
enabled = userScrollEnabled, | ||
) | ||
.scrollable( | ||
orientation = Orientation.Horizontal, | ||
interactionSource = state.internalInteractionSource, | ||
reverseDirection = ScrollableDefaults.reverseDirection( | ||
LocalLayoutDirection.current, | ||
Orientation.Horizontal, | ||
false, | ||
), | ||
flingBehavior = flingBehavior, | ||
state = state.horizontalScrollableState, | ||
overscrollEffect = overscrollEffect, | ||
enabled = userScrollEnabled, | ||
) | ||
.clip(RectangleShape), | ||
prefetchState = state.prefetchState, | ||
measurePolicy = measurePolicy, | ||
itemProvider = itemProvider, | ||
) | ||
} | ||
|
||
@Composable | ||
private fun rememberLazyTabletMeasurePolicy( | ||
itemProvider: LazyTableItemProvider, | ||
cellSize: LazyTableCellSize, | ||
state: LazyTableState, | ||
pinnedColumns: Int, | ||
pinnedRows: Int, | ||
contentPadding: PaddingValues, | ||
beyondBoundsItemCount: Int, | ||
horizontalArrangement: Arrangement.Horizontal? = null, | ||
verticalArrangement: Arrangement.Vertical? = null, | ||
): LazyLayoutMeasureScope.(Constraints) -> MeasureResult = | ||
remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>( | ||
itemProvider, | ||
state, | ||
pinnedColumns, | ||
pinnedRows, | ||
contentPadding, | ||
beyondBoundsItemCount, | ||
) { | ||
{ | ||
containerConstraints -> | ||
check(containerConstraints.maxHeight != Constraints.Infinity && containerConstraints.maxWidth != Constraints.Infinity) { | ||
"LazyTable does not support infinite constraints." | ||
} | ||
|
||
state.density = this | ||
|
||
val startPadding = contentPadding.calculateStartPadding(layoutDirection).roundToPx() | ||
val endPadding = contentPadding.calculateEndPadding(layoutDirection).roundToPx() | ||
val topPadding = contentPadding.calculateTopPadding().roundToPx() | ||
val bottomPadding = contentPadding.calculateBottomPadding().roundToPx() | ||
|
||
val totalVerticalPadding = topPadding + bottomPadding | ||
val totalHorizontalPadding = startPadding + endPadding | ||
|
||
val contentConstraints = containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding) | ||
|
||
val availableSize = IntSize( | ||
containerConstraints.maxWidth - totalHorizontalPadding, | ||
containerConstraints.maxHeight - totalVerticalPadding, | ||
) | ||
|
||
val visualItemOffset = IntOffset(startPadding, topPadding) | ||
|
||
val horizontalSpacing = horizontalArrangement?.spacing?.roundToPx() ?: 0 | ||
val verticalSpacing = verticalArrangement?.spacing?.roundToPx() ?: 0 | ||
|
||
val measuredItemProvider = object : LazyTableMeasuredItemProvider( | ||
availableSize = availableSize, | ||
rows = itemProvider.rowCount, | ||
columns = itemProvider.columnCount, | ||
cellSize = cellSize, | ||
horizontalSpacing = horizontalSpacing, | ||
verticalSpacing = verticalSpacing, | ||
itemProvider = itemProvider, | ||
measureScope = this, | ||
density = this | ||
) { | ||
override fun createItem( | ||
column: Int, | ||
row: Int, | ||
size: IntSize, | ||
key: Any, | ||
contentType: Any?, | ||
placeables: List<Placeable>, | ||
): LazyTableMeasuredItem { | ||
// we add spaceBetweenItems as an extra spacing for all items apart from the last one so | ||
// the lazy list measuring logic will take it into account. | ||
val coordinate = IntOffset(column, row) | ||
val index = itemProvider.getIndex(coordinate) | ||
return LazyTableMeasuredItem( | ||
index = index, | ||
row = row, | ||
column = column, | ||
size = size, | ||
placeables = placeables, | ||
alignment = Alignment.TopStart, | ||
layoutDirection = layoutDirection, | ||
visualOffset = visualItemOffset, | ||
key = key, | ||
contentType = contentType, | ||
) | ||
} | ||
} | ||
|
||
val firstVisibleItemCoordinate: IntOffset | ||
val firstVisibleScrollOffset: IntOffset | ||
|
||
Snapshot.withoutReadObservation { | ||
firstVisibleItemCoordinate = IntOffset( | ||
state.firstVisibleColumnIndex, | ||
state.firstVisibleRowIndex, | ||
) | ||
firstVisibleScrollOffset = IntOffset( | ||
state.firstVisibleItemHorizontalScrollOffset, | ||
state.firstVisibleItemVerticalScrollOffset, | ||
) | ||
} | ||
|
||
measureLazyTable( | ||
constraints = contentConstraints, | ||
availableSize = availableSize, | ||
rows = itemProvider.rowCount, | ||
columns = itemProvider.columnCount, | ||
cellSize = cellSize, | ||
pinnedColumns = minOf(pinnedColumns, itemProvider.columnCount), | ||
pinnedRows = minOf(pinnedRows, itemProvider.rowCount), | ||
measuredItemProvider = measuredItemProvider, | ||
horizontalSpacing = horizontalSpacing, | ||
verticalSpacing = verticalSpacing, | ||
firstVisibleCellPosition = firstVisibleItemCoordinate, | ||
firstVisibleCellScrollOffset = firstVisibleScrollOffset, | ||
scrollToBeConsumed = Offset(state.scrollToBeConsumedHorizontal, state.scrollToBeConsumedVertical), | ||
density = this, | ||
beyondBoundsItemCount = beyondBoundsItemCount, | ||
layout = { width, height, placement -> | ||
layout( | ||
containerConstraints.constrainWidth(width + totalHorizontalPadding), | ||
containerConstraints.constrainHeight(height + totalVerticalPadding), | ||
emptyMap(), | ||
placement, | ||
) | ||
}, | ||
).also { | ||
state.applyMeasureResult(it) | ||
} | ||
} | ||
} | ||
|
||
public interface LazyTableRowScope { | ||
|
||
public fun column(column: Int, content: @Composable (LazyTableCellState) -> Unit) | ||
} | ||
|
||
public interface LazyTableColumnScope { | ||
|
||
public fun row(row: Int, content: @Composable (LazyTableCellState) -> Unit) | ||
} | ||
|
||
public interface LazyTableCellState { | ||
|
||
public val row: Int | ||
|
||
public val column: Int | ||
|
||
public val selected: Boolean | ||
} |
Oops, something went wrong.