diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d618902..5b407d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ androidGradlePlugin = "8.5.2" androidxActivity = "1.9.2" activity-compose = "1.9.2" androidxAppCompat = "1.7.0" -androidxBenchmark = "1.3.0" +androidxBenchmark = "1.3.3" androidxCore = "1.13.1" androidxCompose = "1.7.0" androidxPaging = "3.3.2" @@ -12,16 +12,16 @@ androidxTestExt = "1.2.1" androidxTestRunner = "1.6.2" androidxTestRules = "1.6.1" dokka = "1.8.20" -jetbrainsCompose = "1.7.0-rc01" -jetbrainsLifecycle = "2.8.2" -jetbrainsMaterial3Adaptive = "1.0.0-rc01" +jetbrainsCompose = "1.7.0" +jetbrainsLifecycle = "2.8.3" +jetbrainsMaterial3Adaptive = "1.0.0" junit4 = "4.13.2" kotlin = "2.0.20" kotlinxCoroutines = "1.9.0" kotlinxDatetime = "0.6.1" lifecycle-runtime = "2.8.6" tunjidStateHolder = "1.1.0" -tunjidComposables = "0.0.5" +tunjidComposables = "0.0.7" junit = "4.13.2" runner = "1.0.2" espressoCore = "3.0.2" diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt index 2fc0e72..648fc5c 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/SavedStatePanedNavHostState.kt @@ -37,7 +37,7 @@ import com.tunjid.treenav.traverse interface PanedNavHostState { /** - * Creates the scope that provides context about individual panes [Pane] in an [PanedNavHost]. + * Creates the scope that provides context about individual panes [Pane] in a [PanedNavHost]. */ @Composable fun scope(): PanedNavHostScope diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt index bdcb800..b6087cd 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/threepane/configurations/ThreePaneAdaptiveConfiguration.kt @@ -3,6 +3,8 @@ package com.tunjid.treenav.compose.threepane.configurations import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tunjid.treenav.Node import com.tunjid.treenav.compose.PanedNavHostConfiguration import com.tunjid.treenav.compose.delegated @@ -10,24 +12,24 @@ import com.tunjid.treenav.compose.threepane.ThreePane /** * An [PanedNavHostConfiguration] that selectively displays panes for a [ThreePane] layout - * based on the space available determined by the [windowWidthDpState]. + * based on the space available determined by the [windowWidthState]. * - * @param windowWidthDpState provides the current width of the display in Dp. + * @param windowWidthState provides the current width of the display in Dp. */ fun PanedNavHostConfiguration< ThreePane, NavigationState, Destination >.threePanedNavHostConfiguration( - windowWidthDpState: State, - secondaryPaneBreakPoint: State = mutableStateOf(SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP), - tertiaryPaneBreakPoint: State = mutableStateOf(TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP), + windowWidthState: State, + secondaryPaneBreakPoint: State = mutableStateOf(SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP), + tertiaryPaneBreakPoint: State = mutableStateOf(TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP), ): PanedNavHostConfiguration = delegated { destination -> val originalStrategy = strategyTransform(destination) originalStrategy.delegated( paneMapping = { navigationDestinationToMap -> // Consider navigation state different if window size class changes - val windowWidthDp by windowWidthDpState + val windowWidthDp by windowWidthState val originalMapping = originalStrategy.paneMapper(navigationDestinationToMap) val primaryNode = originalMapping[ThreePane.Primary] mapOf( @@ -45,5 +47,5 @@ fun PanedNavHostConfiguration< ) } -private const val SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 600 -private const val TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 1200 \ No newline at end of file +private val SECONDARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 600.dp +private val TERTIARY_PANE_MIN_WIDTH_BREAKPOINT_DP = 1200.dp \ No newline at end of file diff --git a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/utilities/AnimatedBoundsModifier.kt b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/utilities/AnimatedBoundsModifier.kt index 87c330c..373bc8b 100644 --- a/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/utilities/AnimatedBoundsModifier.kt +++ b/library/compose/src/commonMain/kotlin/com/tunjid/treenav/compose/utilities/AnimatedBoundsModifier.kt @@ -283,7 +283,7 @@ internal class BoundsTransformDeferredAnimation { // Find the given lookahead coordinates by traversing up the tree while (currentCoords.toLookaheadCoordinates() != lookaheadScopeCoordinates) { - if (currentCoords.introducesMotionFrameOfReference) { + if (currentCoords.isAttached && currentCoords.introducesMotionFrameOfReference) { if (parents.size == index) { parents.add(currentCoords) delta += currentCoords.positionInParent() @@ -294,11 +294,13 @@ internal class BoundsTransformDeferredAnimation { } index++ } - currentCoords = currentCoords.parentCoordinates ?: break + currentCoords = currentCoords.parentCoordinates + ?.takeIf(LayoutCoordinates::isAttached) + ?: break } for (i in parents.size - 1 downTo index) { - delta -= parents[i].positionInParent() + if (parents[i].isAttached) delta -= parents[i].positionInParent() parents.removeAt(parents.size - 1) } directManipulationParents = parents diff --git a/libraryVersion.properties b/libraryVersion.properties index d2b2934..1a55704 100644 --- a/libraryVersion.properties +++ b/libraryVersion.properties @@ -14,6 +14,6 @@ # limitations under the License. # groupId=com.tunjid.treenav -treenav_version=0.0.7 -strings_version=0.0.7 -compose_version=0.0.7 \ No newline at end of file +treenav_version=0.0.9 +strings_version=0.0.9 +compose_version=0.0.9 \ No newline at end of file diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt index 6c21446..6cce4f5 100644 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt +++ b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/DemoApp.kt @@ -52,7 +52,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -61,13 +60,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round +import com.tunjid.composables.splitlayout.SplitLayout +import com.tunjid.composables.splitlayout.SplitLayoutState import com.tunjid.demo.common.ui.SampleAppState.Companion.rememberPanedNavHostState import com.tunjid.demo.common.ui.chat.chatPaneStrategy import com.tunjid.demo.common.ui.chatrooms.chatRoomPaneStrategy @@ -95,7 +95,6 @@ import com.tunjid.treenav.popToRoot import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.math.roundToInt @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -115,7 +114,19 @@ fun SampleApp( } ) { SharedTransitionScope { sharedTransitionModifier -> - val windowWidthDp = remember { mutableIntStateOf(0) } + val order = remember { + listOf( + ThreePane.Tertiary, + ThreePane.Secondary, + ThreePane.Primary, + ) + } + val splitLayoutState = remember { + SplitLayoutState( + orientation = Orientation.Horizontal, + maxCount = order.size, + ) + } val surfaceColor = MaterialTheme.colorScheme.surfaceColorAtElevation( animateDpAsState(if (appState.predictiveBackStatus.value) 16.dp else 0.dp).value ) @@ -134,6 +145,8 @@ fun SampleApp( } PanedNavHost( + modifier = Modifier + .fillMaxSize(), state = appState.rememberPanedNavHostState { this .paneModifierConfiguration { @@ -148,7 +161,9 @@ fun SampleApp( .fillMaxSize() } .threePanedNavHostConfiguration( - windowWidthDpState = windowWidthDp + windowWidthState = derivedStateOf { + splitLayoutState.size + } ) .predictiveBackConfiguration( isPreviewingBack = appState.predictiveBackStatus, @@ -172,38 +187,20 @@ fun SampleApp( } ) }, - modifier = Modifier - .fillMaxSize() - .onSizeChanged { - windowWidthDp.value = (it.width / density.density).roundToInt() - } ) { - val order = remember { - listOf( - ThreePane.Tertiary, - ThreePane.Secondary, - ThreePane.Primary, - ) - } val filteredOrder by remember { derivedStateOf { order.filter { nodeFor(it) != null } } } - val segmentedLayoutState = remember { - SegmentedLayoutState( - totalCount = order.size, - ) - }.also { - it.visibleCount = filteredOrder.size - } - SegmentedLayout( - state = segmentedLayoutState, + splitLayoutState.visibleCount = filteredOrder.size + SplitLayout( + state = splitLayoutState, modifier = Modifier .fillMaxSize() then movableSharedElementHostState.modifier then sharedTransitionModifier, itemSeparators = { paneIndex, offset -> PaneSeparator( - segmentedLayoutState = segmentedLayoutState, + splitLayoutState = splitLayoutState, interactionSource = appState.paneInteractionSourceAt(paneIndex), index = paneIndex, density = density, @@ -223,7 +220,7 @@ fun SampleApp( @Composable private fun PaneSeparator( - segmentedLayoutState: SegmentedLayoutState, + splitLayoutState: SplitLayoutState, interactionSource: MutableInteractionSource, modifier: Modifier = Modifier, index: Int, @@ -232,7 +229,7 @@ private fun PaneSeparator( ) { var alpha by remember { mutableFloatStateOf(0f) } val draggableState = rememberDraggableState { - segmentedLayoutState.dragBy( + splitLayoutState.dragBy( index = index, delta = with(density) { it.toDp() } ) diff --git a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt b/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt deleted file mode 100644 index d8f1f15..0000000 --- a/sample/common/src/commonMain/kotlin/com/tunjid/demo/common/ui/SegmentedLayout.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.tunjid.demo.common.ui - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.times -import androidx.compose.ui.unit.toSize - -@Stable -class SegmentedLayoutState( - val totalCount: Int, - visibleCount: Int = totalCount, - minWidth: Dp = 80.dp, -) { - var visibleCount by mutableIntStateOf(visibleCount) - var minWidth by mutableStateOf(minWidth) - var width by mutableStateOf(DpSize.Zero.width) - internal set - - private val weightMap = mutableStateMapOf().apply { - (0.. put(index, 1f / totalCount) } - } - - fun weightAt(index: Int): Float = weightMap.getValue(index) - - fun setWeightAt(index: Int, weight: Float) { - if (weight * width < minWidth) return - - val oldWeight = weightMap.getValue(index) - val weightDifference = oldWeight - weight - val adjustedIndex = (0.. - val searchIndex = (index + i) % totalCount - if (searchIndex == index) return@search null - - val adjustedWidth = (weightMap.getValue(searchIndex) + weightDifference) * width - if (adjustedWidth < minWidth) return@search null - - searchIndex - } ?: return - - weightMap[index] = weight - weightMap[adjustedIndex] = weightMap.getValue(adjustedIndex) + weightDifference - } - - fun isVisibleAt(index: Int) = index < visibleCount - - fun dragBy(index: Int, delta: Dp) { - val oldWeight = weightAt(index) - val width = oldWeight * width - val newWidth = width + delta - val newWeight = newWidth / this.width - setWeightAt(index = index, weight = newWeight) - } - - internal companion object SegmentedLayoutInstance { - - @Composable - fun SegmentedLayoutState.Separators( - separator: @Composable (paneIndex: Int, offset: Dp) -> Unit - ) { - val totalWeight by remember { - derivedStateOf { - (0.. - val previousIndexOffset = - if (index == 0) 0.dp - else (weightAt(index - 1) / totalWeight) * width - val indexOffset = (weightAt(index) / totalWeight) * width - previousIndexOffset + indexOffset - } - } - } - if (visibleCount > 1) - for (index in 0.. Unit = { _, _ -> }, - itemContent: @Composable (Int) -> Unit, -) = with(SegmentedLayoutState) { - val density = LocalDensity.current - Box( - modifier = modifier - .onSizeChanged { - state.updateSize(it, density) - }, - ) { - Row( - modifier = Modifier - .matchParentSize(), - ) { - for (index in 0..