Rebase interactive is shown on top of log instead of taking all the space

This commit is contained in:
Abdelilah El Aissaoui 2023-09-07 14:04:26 +02:00
parent 117c5f4fd2
commit 1778f69622
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
4 changed files with 240 additions and 30 deletions

View File

@ -1,9 +1,12 @@
package com.jetpackduba.gitnuro.ui package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items 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.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -18,7 +21,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.theme.backgroundSelected 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.RebaseAction
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewModel
import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewState import com.jetpackduba.gitnuro.viewmodels.RebaseInteractiveViewState
@ -64,6 +71,7 @@ fun RebaseInteractive(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun RebaseStateLoaded( fun RebaseStateLoaded(
rebaseInteractiveViewModel: RebaseInteractiveViewModel, rebaseInteractiveViewModel: RebaseInteractiveViewModel,
@ -84,7 +92,10 @@ fun RebaseStateLoaded(
fontSize = 20.sp, fontSize = 20.sp,
) )
ScrollableLazyColumn(modifier = Modifier.weight(1f)) { ScrollableLazyColumn(
modifier = Modifier
.weight(1f)
) {
items(stepsList) { rebaseTodoLine -> items(stepsList) { rebaseTodoLine ->
RebaseCommit( RebaseCommit(
rebaseLine = rebaseTodoLine, rebaseLine = rebaseTodoLine,

View File

@ -255,8 +255,6 @@ fun MainContentView(
) { ) {
val rebaseInteractiveState by tabViewModel.rebaseInteractiveState.collectAsState() val rebaseInteractiveState by tabViewModel.rebaseInteractiveState.collectAsState()
println("Rebase interactive state is $rebaseInteractiveState")
HorizontalSplitPane( HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(initialPositionPercentage = 0.20f) splitPaneState = rememberSplitPaneState(initialPositionPercentage = 0.20f)
) { ) {
@ -282,7 +280,7 @@ fun MainContentView(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction) { if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction && diffSelected == null) {
RebaseInteractive() RebaseInteractive()
} else if (blameState is BlameState.Loaded && !blameState.isMinimized) { } else if (blameState is BlameState.Loaded && !blameState.isMinimized) {
Blame( Blame(

View File

@ -27,9 +27,9 @@ import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.onMiddleMouseButtonClick import com.jetpackduba.gitnuro.extensions.onMiddleMouseButtonClick
import com.jetpackduba.gitnuro.managers.AppStateManager import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.ui.drag_sorting.DraggableItem import com.jetpackduba.gitnuro.ui.drag_sorting.HorizontalDraggableItem
import com.jetpackduba.gitnuro.ui.drag_sorting.dragContainer import com.jetpackduba.gitnuro.ui.drag_sorting.horizontalDragContainer
import com.jetpackduba.gitnuro.ui.drag_sorting.rememberDragDropState import com.jetpackduba.gitnuro.ui.drag_sorting.rememberHorizontalDragDropState
import com.jetpackduba.gitnuro.viewmodels.TabViewModel import com.jetpackduba.gitnuro.viewmodels.TabViewModel
import com.jetpackduba.gitnuro.viewmodels.TabViewModelsHolder import com.jetpackduba.gitnuro.viewmodels.TabViewModelsHolder
import kotlinx.coroutines.delay 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) onMoveTab(fromIndex, toIndex)
} }
@ -97,14 +97,19 @@ fun RepositoriesTabPanel(
modifier = Modifier modifier = Modifier
.height(tabsHeight) .height(tabsHeight)
.weight(1f, false) .weight(1f, false)
.dragContainer(dragDropState), .horizontalDragContainer(dragDropState),
state = stateHorizontal, state = stateHorizontal,
) { ) {
itemsIndexed( itemsIndexed(
items = tabs, items = tabs,
key = { _, tab -> tab.tabViewModel } key = { _, tab -> tab.tabViewModel }
) { index, tab -> ) { index, tab ->
DraggableItem(dragDropState, index) { _ -> HorizontalDraggableItem(dragDropState, index) { isDragged ->
LaunchedEffect(isDragged) {
if (isDragged) {
onTabSelected(tab)
}
}
Tooltip(tab.path) { Tooltip(tab.path) {
Tab( Tab(
modifier = Modifier, modifier = Modifier,

View File

@ -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.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi 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.gestures.scrollBy
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
@ -15,7 +17,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@ -23,14 +24,14 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun rememberDragDropState( fun rememberHorizontalDragDropState(
lazyListState: LazyListState, lazyListState: LazyListState,
@Suppress("PrimitiveInLambda") @Suppress("PrimitiveInLambda")
onMove: (Int, Int) -> Unit onMove: (Int, Int) -> Unit
): DragDropState { ): HorizontalDragDropState {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = remember(lazyListState) { val state = remember(lazyListState) {
DragDropState( HorizontalDragDropState(
state = lazyListState, state = lazyListState,
onMove = onMove, onMove = onMove,
scope = scope scope = scope
@ -45,7 +46,30 @@ fun rememberDragDropState(
return state 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 state: LazyListState,
private val scope: CoroutineScope, private val scope: CoroutineScope,
@Suppress("PrimitiveInLambda") @Suppress("PrimitiveInLambda")
@ -116,6 +140,7 @@ class DragDropState internal constructor(
middleOffset.toInt() in item.offset..item.offsetEnd && middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index draggingItem.index != item.index
} }
if (targetItem != null) { if (targetItem != null) {
val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) { val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
draggingItem.index draggingItem.index
@ -138,8 +163,10 @@ class DragDropState internal constructor(
val overscroll = when { val overscroll = when {
draggingItemDraggedDelta > 0 -> draggingItemDraggedDelta > 0 ->
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
draggingItemDraggedDelta < 0 -> draggingItemDraggedDelta < 0 ->
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
else -> 0f else -> 0f
} }
if (overscroll != 0f) { if (overscroll != 0f) {
@ -152,29 +179,168 @@ class DragDropState internal constructor(
get() = this.offset + this.size get() = this.offset + this.size
} }
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier { class VerticalDragDropState internal constructor(
return pointerInput(dragDropState) { private val state: LazyListState,
detectDragGestures( private val scope: CoroutineScope,
onDrag = { change, offset -> @Suppress("PrimitiveInLambda")
change.consume() private val onMove: (Int, Int) -> Unit
dragDropState.onDrag(offset = offset) ) {
}, var draggingItemIndex by mutableStateOf<Int?>(null)
onDragStart = { offset -> dragDropState.onDragStart(offset) }, private set
onDragEnd = { dragDropState.onDragInterrupted() },
onDragCancel = { dragDropState.onDragInterrupted() } internal val scrollChannel = Channel<Float>()
)
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<Int?>(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 @ExperimentalFoundationApi
@Composable @Composable
fun LazyItemScope.DraggableItem( fun LazyItemScope.HorizontalDraggableItem(
dragDropState: DragDropState, dragDropState: HorizontalDragDropState,
index: Int, index: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
) { ) {
val dragging = index == dragDropState.draggingItemIndex val dragging = index == dragDropState.draggingItemIndex
val draggingModifier = if (dragging) { val draggingModifier = if (dragging) {
Modifier Modifier
.zIndex(1f) .zIndex(1f)
@ -193,3 +359,33 @@ fun LazyItemScope.DraggableItem(
content(dragging) 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)
}
}