From 1778f69622014f78d06b463dfe3150d6e0a90665 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Thu, 7 Sep 2023 14:04:26 +0200 Subject: [PATCH] Rebase interactive is shown on top of log instead of taking all the space --- .../gitnuro/ui/RebaseInteractive.kt | 15 +- .../jetpackduba/gitnuro/ui/RepositoryOpen.kt | 4 +- .../ui/components/RepositoriesTabPanel.kt | 17 +- .../gitnuro/ui/drag_sorting/RowDragSorting.kt | 234 ++++++++++++++++-- 4 files changed, 240 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt index 6d26b74..266ed1d 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RebaseInteractive.kt @@ -1,9 +1,12 @@ package com.jetpackduba.gitnuro.ui +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -18,7 +21,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.theme.backgroundSelected -import com.jetpackduba.gitnuro.ui.components.* +import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField +import com.jetpackduba.gitnuro.ui.components.PrimaryButton +import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn +import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel +import com.jetpackduba.gitnuro.ui.drag_sorting.rememberVerticalDragDropState import com.jetpackduba.gitnuro.viewmodels.RebaseAction import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewState @@ -64,6 +71,7 @@ fun RebaseInteractive( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun RebaseStateLoaded( rebaseInteractiveViewModel: RebaseInteractiveViewModel, @@ -84,7 +92,10 @@ fun RebaseStateLoaded( fontSize = 20.sp, ) - ScrollableLazyColumn(modifier = Modifier.weight(1f)) { + ScrollableLazyColumn( + modifier = Modifier + .weight(1f) + ) { items(stepsList) { rebaseTodoLine -> RebaseCommit( rebaseLine = rebaseTodoLine, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt index a2bc8b0..7191749 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt @@ -255,8 +255,6 @@ fun MainContentView( ) { val rebaseInteractiveState by tabViewModel.rebaseInteractiveState.collectAsState() - println("Rebase interactive state is $rebaseInteractiveState") - HorizontalSplitPane( splitPaneState = rememberSplitPaneState(initialPositionPercentage = 0.20f) ) { @@ -282,7 +280,7 @@ fun MainContentView( modifier = Modifier .fillMaxSize() ) { - if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction) { + if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction && diffSelected == null) { RebaseInteractive() } else if (blameState is BlameState.Loaded && !blameState.isMinimized) { Blame( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt index 980e05e..f0688b2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt @@ -27,9 +27,9 @@ import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handOnHover import com.jetpackduba.gitnuro.extensions.onMiddleMouseButtonClick import com.jetpackduba.gitnuro.managers.AppStateManager -import com.jetpackduba.gitnuro.ui.drag_sorting.DraggableItem -import com.jetpackduba.gitnuro.ui.drag_sorting.dragContainer -import com.jetpackduba.gitnuro.ui.drag_sorting.rememberDragDropState +import com.jetpackduba.gitnuro.ui.drag_sorting.HorizontalDraggableItem +import com.jetpackduba.gitnuro.ui.drag_sorting.horizontalDragContainer +import com.jetpackduba.gitnuro.ui.drag_sorting.rememberHorizontalDragDropState import com.jetpackduba.gitnuro.viewmodels.TabViewModel import com.jetpackduba.gitnuro.viewmodels.TabViewModelsHolder import kotlinx.coroutines.delay @@ -88,7 +88,7 @@ fun RepositoriesTabPanel( } - val dragDropState = rememberDragDropState(stateHorizontal) { fromIndex, toIndex -> + val dragDropState = rememberHorizontalDragDropState(stateHorizontal) { fromIndex, toIndex -> onMoveTab(fromIndex, toIndex) } @@ -97,14 +97,19 @@ fun RepositoriesTabPanel( modifier = Modifier .height(tabsHeight) .weight(1f, false) - .dragContainer(dragDropState), + .horizontalDragContainer(dragDropState), state = stateHorizontal, ) { itemsIndexed( items = tabs, key = { _, tab -> tab.tabViewModel } ) { index, tab -> - DraggableItem(dragDropState, index) { _ -> + HorizontalDraggableItem(dragDropState, index) { isDragged -> + LaunchedEffect(isDragged) { + if (isDragged) { + onTabSelected(tab) + } + } Tooltip(tab.path) { Tab( modifier = Modifier, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/drag_sorting/RowDragSorting.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/drag_sorting/RowDragSorting.kt index 7c394dc..b5ce81a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/drag_sorting/RowDragSorting.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/drag_sorting/RowDragSorting.kt @@ -4,7 +4,9 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -15,7 +17,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.zIndex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -23,14 +24,14 @@ import kotlinx.coroutines.launch @Composable -fun rememberDragDropState( +fun rememberHorizontalDragDropState( lazyListState: LazyListState, @Suppress("PrimitiveInLambda") onMove: (Int, Int) -> Unit -): DragDropState { +): HorizontalDragDropState { val scope = rememberCoroutineScope() val state = remember(lazyListState) { - DragDropState( + HorizontalDragDropState( state = lazyListState, onMove = onMove, scope = scope @@ -45,7 +46,30 @@ fun rememberDragDropState( return state } -class DragDropState internal constructor( +@Composable +fun rememberVerticalDragDropState( + lazyListState: LazyListState, + @Suppress("PrimitiveInLambda") + onMove: (Int, Int) -> Unit +): VerticalDragDropState { + val scope = rememberCoroutineScope() + val state = remember(lazyListState) { + VerticalDragDropState( + state = lazyListState, + onMove = onMove, + scope = scope + ) + } + LaunchedEffect(state) { + while (true) { + val diff = state.scrollChannel.receive() + lazyListState.scrollBy(diff) + } + } + return state +} + +class HorizontalDragDropState internal constructor( private val state: LazyListState, private val scope: CoroutineScope, @Suppress("PrimitiveInLambda") @@ -116,6 +140,7 @@ class DragDropState internal constructor( middleOffset.toInt() in item.offset..item.offsetEnd && draggingItem.index != item.index } + if (targetItem != null) { val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) { draggingItem.index @@ -138,8 +163,10 @@ class DragDropState internal constructor( val overscroll = when { draggingItemDraggedDelta > 0 -> (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + draggingItemDraggedDelta < 0 -> (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + else -> 0f } if (overscroll != 0f) { @@ -152,29 +179,168 @@ class DragDropState internal constructor( get() = this.offset + this.size } -fun Modifier.dragContainer(dragDropState: DragDropState): Modifier { - return pointerInput(dragDropState) { - detectDragGestures( - onDrag = { change, offset -> - change.consume() - dragDropState.onDrag(offset = offset) - }, - onDragStart = { offset -> dragDropState.onDragStart(offset) }, - onDragEnd = { dragDropState.onDragInterrupted() }, - onDragCancel = { dragDropState.onDragInterrupted() } - ) +class VerticalDragDropState internal constructor( + private val state: LazyListState, + private val scope: CoroutineScope, + @Suppress("PrimitiveInLambda") + private val onMove: (Int, Int) -> Unit +) { + var draggingItemIndex by mutableStateOf(null) + private set + + internal val scrollChannel = Channel() + + private var draggingItemDraggedDelta by mutableStateOf(0f) + private var draggingItemInitialOffset by mutableStateOf(0) + internal val draggingItemOffset: Float + get() = draggingItemLayoutInfo?.let { item -> + draggingItemInitialOffset + draggingItemDraggedDelta - item.offset + } ?: 0f + + private val draggingItemLayoutInfo: LazyListItemInfo? + get() = state.layoutInfo.visibleItemsInfo + .firstOrNull { it.index == draggingItemIndex } + + internal var previousIndexOfDraggedItem by mutableStateOf(null) + private set + internal var previousItemOffset = Animatable(0f) + private set + + internal fun onDragStart(offset: Offset) { + state.layoutInfo.visibleItemsInfo + .firstOrNull { item -> + offset.y.toInt() in item.offset..(item.offset + item.size) + }?.also { + draggingItemIndex = it.index + draggingItemInitialOffset = it.offset + } } + + internal fun onDragInterrupted() { + if (draggingItemIndex != null) { + previousIndexOfDraggedItem = draggingItemIndex + val startOffset = draggingItemOffset + scope.launch { + previousItemOffset.snapTo(startOffset) + previousItemOffset.animateTo( + 0f, + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = 1f + ) + ) + previousIndexOfDraggedItem = null + } + } + draggingItemDraggedDelta = 0f + draggingItemIndex = null + draggingItemInitialOffset = 0 + } + + internal fun onDrag(offset: Offset) { + draggingItemDraggedDelta += offset.y + + val draggingItem = draggingItemLayoutInfo ?: return + val startOffset = draggingItem.offset + draggingItemOffset + val endOffset = startOffset + draggingItem.size + val middleOffset = startOffset + (endOffset - startOffset) / 2f + println("Middle offset is $middleOffset") + + val targetItem = state.layoutInfo.visibleItemsInfo.find { item -> + middleOffset.toInt() in item.offset..item.offsetEnd && + draggingItem.index != item.index + } + + if (targetItem != null) { + val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) { + draggingItem.index + } else if (draggingItem.index == state.firstVisibleItemIndex) { + targetItem.index + } else { + null + } + if (scrollToIndex != null) { + scope.launch { + // this is needed to neutralize automatic keeping the first item first. + state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset) + onMove.invoke(draggingItem.index, targetItem.index) + } + } else { + onMove.invoke(draggingItem.index, targetItem.index) + } + draggingItemIndex = targetItem.index + } else { + val overscroll = when { + draggingItemDraggedDelta > 0 -> + (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + + draggingItemDraggedDelta < 0 -> + (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + + else -> 0f + } + if (overscroll != 0f) { + scrollChannel.trySend(overscroll) + } + } + } + + private val LazyListItemInfo.offsetEnd: Int + get() = this.offset + this.size +} + +@Composable +fun Modifier.horizontalDragContainer(dragDropState: HorizontalDragDropState): Modifier { + val state = rememberDraggableState { + println("Dragging horizontally $it") + dragDropState.onDrag(Offset(it, 0f)) + } + + return this.draggable( + state = state, + orientation = Orientation.Horizontal, + startDragImmediately = false, + onDragStarted = { + dragDropState.onDragStart(it) + }, + onDragStopped = { + println("On drag stopped") + dragDropState.onDragInterrupted() + }, + ) +} + +@Composable +fun Modifier.verticalDragContainer(dragDropState: VerticalDragDropState): Modifier { + val state = rememberDraggableState { + println("Dragging vertically $it") + dragDropState.onDrag(Offset(0f, it)) + } + + return this.draggable( + state = state, + orientation = Orientation.Vertical, + startDragImmediately = false, + onDragStarted = { + dragDropState.onDragStart(it) + }, + onDragStopped = { + println("On drag stopped") + dragDropState.onDragInterrupted() + }, + ) } @ExperimentalFoundationApi @Composable -fun LazyItemScope.DraggableItem( - dragDropState: DragDropState, +fun LazyItemScope.HorizontalDraggableItem( + dragDropState: HorizontalDragDropState, index: Int, modifier: Modifier = Modifier, content: @Composable ColumnScope.(isDragging: Boolean) -> Unit ) { val dragging = index == dragDropState.draggingItemIndex + val draggingModifier = if (dragging) { Modifier .zIndex(1f) @@ -193,3 +359,33 @@ fun LazyItemScope.DraggableItem( content(dragging) } } + + +@ExperimentalFoundationApi +@Composable +fun LazyItemScope.VerticalDraggableItem( + dragDropState: VerticalDragDropState, + index: Int, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.(isDragging: Boolean) -> Unit +) { + val dragging = index == dragDropState.draggingItemIndex + + val draggingModifier = if (dragging) { + Modifier + .zIndex(1f) + .graphicsLayer { + translationY = dragDropState.draggingItemOffset + } + } else if (index == dragDropState.previousIndexOfDraggedItem) { + Modifier.zIndex(1f) + .graphicsLayer { + translationY = dragDropState.previousItemOffset.value + } + } else { + Modifier + } + Column(modifier = modifier.then(draggingModifier)) { + content(dragging) + } +}