diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 198563f..87e9bce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,12 +17,12 @@ jobs: runs-on: macos-latest steps: - name: Check out code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v6 - name: Set up JDK - uses: actions/setup-java@v4.3.0 + uses: actions/setup-java@v5.0.0 with: - distribution: 'corretto' - java-version: 17 + distribution: 'zulu' + java-version: 21 - name: spotless run: ./gradlew spotlessCheck @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v6 - name: Java and Gradle set up if: matrix.platform != 'ios' diff --git a/.github/workflows/setup/java-setup/action.yml b/.github/workflows/setup/java-setup/action.yml index b8ef060..0c782a6 100644 --- a/.github/workflows/setup/java-setup/action.yml +++ b/.github/workflows/setup/java-setup/action.yml @@ -4,12 +4,12 @@ runs: using: "composite" steps: # Setup java - - name: Setup JDK 17 + - name: Setup JDK 21 id: setup_jdk - uses: actions/setup-java@v4.3.0 + uses: actions/setup-java@v5.0.0 with: - distribution: "corretto" - java-version: 17 + distribution: 'zulu' + java-version: 21 # Grant execute permission for gradlew - name: Grant execute permission for gradlew @@ -18,14 +18,18 @@ runs: run: chmod +x gradlew # Caching gradle packages - - uses: actions/cache@v3 + # TODO: remove temporary workaround after fixed + # temporarily work around https://github.com/actions/runner-images/issues/13341 + # by disabling caching for macOS + - if: ${{ runner.os != 'macOS' }} + uses: actions/cache@v4 name: Cache Gradle for quicker builds id: caching_gradle with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-gradle- diff --git a/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeColumnTest.kt b/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeColumnTest.kt index 55a8e50..393b416 100644 --- a/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeColumnTest.kt +++ b/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeColumnTest.kt @@ -28,10 +28,13 @@ import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertPositionInRootIsEqualTo @@ -41,6 +44,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -244,4 +248,54 @@ class JetLimeColumnTest { items.add("Another Item") composeTestRule.onNodeWithText("Another Item").assertIsDisplayed() } + + @Test + fun jetLimeColumn_ltr_contentIsVisible() { + val itemsList = ItemsList(persistentListOf("Item 1", "Item 2", "Item 3")) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + JetLimeColumn( + itemsList = itemsList, + itemContent = { _, item, pos -> + JetLimeEvent( + style = JetLimeEventDefaults.eventStyle(position = pos), + ) { + Text(text = item, modifier = Modifier.testTag("ColumnItem_$item")) + } + }, + ) + } + } + + composeTestRule.onNodeWithTag("ColumnItem_Item 1").assertIsDisplayed() + composeTestRule.onNodeWithTag("ColumnItem_Item 3").assertIsDisplayed() + } + + @OptIn(ExperimentalComposeApi::class) + @Test + fun jetLimeColumn_extendedEvent_rtl_contentsAreVisible() { + val itemsList = ItemsList(persistentListOf("Item 1")) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + JetLimeColumn( + itemsList = itemsList, + itemContent = { _, item, pos -> + JetLimeExtendedEvent( + style = JetLimeEventDefaults.eventStyle(position = pos), + additionalContent = { + Text(text = "Additional", modifier = Modifier.testTag("ExtendedAdditional")) + }, + ) { + Text(text = item, modifier = Modifier.testTag("ExtendedMain")) + } + }, + ) + } + } + + composeTestRule.onNodeWithTag("ExtendedAdditional").assertIsDisplayed() + composeTestRule.onNodeWithTag("ExtendedMain").assertIsDisplayed() + } } diff --git a/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeRowTest.kt b/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeRowTest.kt index 31767d3..0a9f21d 100644 --- a/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeRowTest.kt +++ b/jetlime/src/androidTest/java/com/pushpal/jetlime/JetLimeRowTest.kt @@ -28,10 +28,13 @@ import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertPositionInRootIsEqualTo @@ -41,6 +44,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -244,4 +248,110 @@ class JetLimeRowTest { items.add("Another Item") composeTestRule.onNodeWithText("Another Item").assertIsDisplayed() } + + @Test + fun jetLimeRow_horizontalEvent_ltr_contentIsVisible() { + val itemsList = ItemsList(persistentListOf("Item 1", "Item 2", "Item 3")) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + JetLimeRow( + itemsList = itemsList, + itemContent = { _, item, pos -> + JetLimeEvent( + style = JetLimeEventDefaults.eventStyle( + position = pos, + pointPlacement = PointPlacement.CENTER, + ), + ) { + Text(text = item, modifier = Modifier.testTag("RowItem_$item")) + } + }, + ) + } + } + + composeTestRule.onNodeWithTag("RowItem_Item 1").assertIsDisplayed() + composeTestRule.onNodeWithTag("RowItem_Item 3").assertIsDisplayed() + } + + @Test + fun jetLimeRow_horizontalEvent_rtl_contentIsVisible() { + val itemsList = ItemsList(persistentListOf("Item 1", "Item 2", "Item 3")) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + JetLimeRow( + itemsList = itemsList, + itemContent = { _, item, pos -> + JetLimeEvent( + style = JetLimeEventDefaults.eventStyle( + position = pos, + pointPlacement = PointPlacement.CENTER, + ), + ) { + Text(text = item, modifier = Modifier.testTag("RowItem_$item")) + } + }, + ) + } + } + + composeTestRule.onNodeWithTag("RowItem_Item 1").assertIsDisplayed() + composeTestRule.onNodeWithTag("RowItem_Item 3").assertIsDisplayed() + } + + @OptIn(ExperimentalComposeApi::class) + @Test + fun jetLimeExtendedEvent_ltr_contentsAreVisible() { + val itemsList = ItemsList(persistentListOf("Item 1")) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + JetLimeColumn( + itemsList = itemsList, + itemContent = { _, item, pos -> + JetLimeExtendedEvent( + style = JetLimeEventDefaults.eventStyle(position = pos), + additionalContent = { + Text(text = "Additional", modifier = Modifier.testTag("ExtendedAdditional_LTR")) + }, + ) { + Text(text = item, modifier = Modifier.testTag("ExtendedMain_LTR")) + } + }, + ) + } + } + + composeTestRule.onNodeWithTag("ExtendedAdditional_LTR").assertIsDisplayed() + composeTestRule.onNodeWithTag("ExtendedMain_LTR").assertIsDisplayed() + } + + @OptIn(ExperimentalComposeApi::class) + @Test + fun jetLimeExtendedEvent_rtl_contentsAreVisible() { + val itemsList = ItemsList(persistentListOf("Item 1")) + + composeTestRule.setContent { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + JetLimeColumn( + itemsList = itemsList, + itemContent = { _, item, pos -> + JetLimeExtendedEvent( + style = JetLimeEventDefaults.eventStyle(position = pos), + additionalContent = { + Text(text = "Additional", modifier = Modifier.testTag("ExtendedAdditional_RTL")) + }, + ) { + Text(text = item, modifier = Modifier.testTag("ExtendedMain_RTL")) + } + }, + ) + } + } + + composeTestRule.onNodeWithTag("ExtendedAdditional_RTL").assertIsDisplayed() + composeTestRule.onNodeWithTag("ExtendedMain_RTL").assertIsDisplayed() + } } diff --git a/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeEvent.kt b/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeEvent.kt index dff1272..642a2b8 100644 --- a/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeEvent.kt +++ b/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeEvent.kt @@ -27,6 +27,7 @@ package com.pushpal.jetlime import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize @@ -43,7 +44,9 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.pushpal.jetlime.Arrangement.HORIZONTAL import com.pushpal.jetlime.Arrangement.VERTICAL @@ -283,26 +286,27 @@ private fun PlaceVerticalEventContent( alignment: VerticalAlignment, content: @Composable () -> Unit, ) { + val leftPad = if (alignment == VerticalAlignment.LEFT) { + // Physical left side padding irrespective of layout direction + style.pointRadius * 2 + jetLimeStyle.contentDistance + } else { + 0.dp + } + val rightPad = if (alignment == VerticalAlignment.RIGHT) { + style.pointRadius * 2 + jetLimeStyle.contentDistance + } else { + 0.dp + } Box( modifier = Modifier .testTag("VerticalEventContentBox") .defaultMinSize(minHeight = style.pointRadius * 2) - .padding( - start = if (alignment == VerticalAlignment.LEFT) { - style.pointRadius * 2 + jetLimeStyle.contentDistance - } else { - 0.dp - }, - end = if (alignment == VerticalAlignment.RIGHT) { - style.pointRadius * 2 + jetLimeStyle.contentDistance - } else { - 0.dp - }, - bottom = if (style.position.isNotEnd()) { - jetLimeStyle.itemSpacing - } else { - 0.dp - }, + // Use absolutePadding so LEFT/RIGHT alignment refers to physical sides, not start/end semantics. + .absolutePadding( + left = leftPad, + right = rightPad, + top = 0.dp, + bottom = if (style.position.isNotEnd()) jetLimeStyle.itemSpacing else 0.dp, ), ) { content() @@ -327,6 +331,8 @@ internal fun HorizontalEvent( ) { val horizontalAlignment = remember { jetLimeStyle.lineHorizontalAlignment } val radiusAnimFactor by calculateRadiusAnimFactor(style) + val layoutDirection = LocalLayoutDirection.current + val isRtl = layoutDirection == LayoutDirection.Rtl Box( modifier = modifier .wrapContentSize() @@ -335,7 +341,7 @@ internal fun HorizontalEvent( HorizontalAlignment.TOP -> style.pointRadius.toPx() HorizontalAlignment.BOTTOM -> this.size.height - style.pointRadius.toPx() } - val xOffset = when (style.pointPlacement) { + val logicalXOffset = when (style.pointPlacement) { PointPlacement.START -> style.pointRadius.toPx() * jetLimeStyle.pointStartFactor PointPlacement.CENTER -> { val effectiveWidth = @@ -351,58 +357,71 @@ internal fun HorizontalEvent( effectiveWidth - style.pointRadius.toPx() * jetLimeStyle.pointStartFactor } } + // Mirror logical offset for RTL so that timeline direction flips horizontally + val xOffset = if (isRtl) size.width - logicalXOffset else logicalXOffset val radius = style.pointRadius.toPx() * radiusAnimFactor val strokeWidth = style.pointStrokeWidth.toPx() // Line if (style.pointPlacement == PointPlacement.CENTER) { - // Left segment (skip for first item) + // Segment towards the "start" of the timeline (left in LTR, right in RTL) if (style.position.isNotStart()) { + val startX = if (isRtl) this.size.width else 0f + val endX = xOffset drawLine( brush = jetLimeStyle.lineBrush, - start = Offset(x = 0f, y = yOffset), - end = Offset(x = xOffset, y = yOffset), + start = Offset(x = startX, y = yOffset), + end = Offset(x = endX, y = yOffset), strokeWidth = jetLimeStyle.lineThickness.toPx(), pathEffect = jetLimeStyle.pathEffect, ) } - // Right segment (skip for last item) + // Segment towards the "end" of the timeline (right in LTR, left in RTL) if (style.position.isNotEnd()) { + val startX = xOffset + val endX = if (isRtl) 0f else this.size.width drawLine( brush = jetLimeStyle.lineBrush, - start = Offset(x = xOffset, y = yOffset), - end = Offset(x = this.size.width, y = yOffset), + start = Offset(x = startX, y = yOffset), + end = Offset(x = endX, y = yOffset), strokeWidth = jetLimeStyle.lineThickness.toPx(), pathEffect = jetLimeStyle.pathEffect, ) } } else if (style.pointPlacement == PointPlacement.END) { + // END placement behaves like CENTER w.r.t connection, but anchored near item edge if (style.position.isNotStart()) { + val startX = if (isRtl) this.size.width else 0f + val endX = xOffset drawLine( brush = jetLimeStyle.lineBrush, - start = Offset(x = 0f, y = yOffset), - end = Offset(x = xOffset, y = yOffset), + start = Offset(x = startX, y = yOffset), + end = Offset(x = endX, y = yOffset), strokeWidth = jetLimeStyle.lineThickness.toPx(), pathEffect = jetLimeStyle.pathEffect, ) } if (style.position.isNotEnd()) { + val startX = xOffset + val endX = if (isRtl) 0f else this.size.width drawLine( brush = jetLimeStyle.lineBrush, - start = Offset(x = xOffset, y = yOffset), - end = Offset(x = this.size.width, y = yOffset), + start = Offset(x = startX, y = yOffset), + end = Offset(x = endX, y = yOffset), strokeWidth = jetLimeStyle.lineThickness.toPx(), pathEffect = jetLimeStyle.pathEffect, ) } } else { - // START placement original behavior + // START placement original behavior, but mirrored for RTL so connectors flow with layout direction if (style.position.isNotEnd()) { val xShift = xOffset * (jetLimeStyle.pointStartFactor - 1) + val startX = xOffset + val endX = if (isRtl) 0f - xShift else this.size.width + xShift drawLine( brush = jetLimeStyle.lineBrush, - start = Offset(x = xOffset, y = yOffset), - end = Offset(x = this.size.width + xShift, y = yOffset), + start = Offset(x = startX, y = yOffset), + end = Offset(x = endX, y = yOffset), strokeWidth = jetLimeStyle.lineThickness.toPx(), pathEffect = jetLimeStyle.pathEffect, ) diff --git a/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeExtendedEvent.kt b/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeExtendedEvent.kt index 97e56de..3acce77 100644 --- a/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeExtendedEvent.kt +++ b/jetlime/src/commonMain/kotlin/com/pushpal/jetlime/JetLimeExtendedEvent.kt @@ -44,7 +44,9 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.pushpal.jetlime.JetLimeEventDefaults.AdditionalContentMaxWidth @@ -91,15 +93,15 @@ fun JetLimeExtendedEvent( content: @Composable () -> Unit, ) { val jetLimeStyle = LocalJetLimeStyle.current - val strokeWidth = with(LocalDensity.current) { style.pointStrokeWidth.toPx() } + val density = LocalDensity.current + val strokeWidth = with(density) { style.pointStrokeWidth.toPx() } val radiusAnimFactor by calculateRadiusAnimFactor(style) + val layoutDirection = LocalLayoutDirection.current + val isRtl = layoutDirection == LayoutDirection.Rtl - // BoxWithConstraints provides its own constraints which we can use for layout BoxWithConstraints(modifier = modifier) { - // Variable for keeping track of the X position where the timeline will be drawn - var timelineXOffset by remember { mutableFloatStateOf(0f) } - // Maximum width for additional content - val maxAdditionalContentWidth = with(LocalDensity.current) { additionalContentMaxWidth.toPx() } + var logicalTimelineXOffset by remember { mutableFloatStateOf(0f) } + val maxAdditionalContentWidth = with(density) { additionalContentMaxWidth.toPx() } Layout( content = { @@ -137,20 +139,24 @@ fun JetLimeExtendedEvent( // Calculating intrinsic width and adjusting it according to the maximum allowed width val intrinsicWidth = measurable.minIntrinsicWidth(constraints.maxHeight) val adjustedMinWidth = intrinsicWidth.coerceAtMost(maxAdditionalContentWidth.toInt()) + // Ensure we do not exceed available width + val maxWidthForAdditional = + maxAdditionalContentWidth.coerceAtMost(constraints.maxWidth.toFloat()).toInt() val newConstraints = constraints.copy( - minWidth = adjustedMinWidth, - maxWidth = maxAdditionalContentWidth.toInt(), + minWidth = adjustedMinWidth.coerceAtMost(maxWidthForAdditional), + maxWidth = maxWidthForAdditional, ) // Measuring the additional content with the new constraints measurable.measure(newConstraints) } - // Calculating the X offset for the timeline based on the width of the additional content - timelineXOffset = (additionalContentPlaceable?.width?.toFloat() ?: 0f) + contentDistance + // Calculating the logical X offset for the timeline based on the width of the additional content + logicalTimelineXOffset = + (additionalContentPlaceable?.width?.toFloat() ?: 0f) + contentDistance - // Calculating the X offset and width available for the main content - val contentXOffset = timelineXOffset + timelineThickness + contentDistance - val contentWidth = constraints.maxWidth - contentXOffset + // Calculating the X offset and width available for the main content in logical LTR space + val logicalContentXOffset = logicalTimelineXOffset + timelineThickness + contentDistance + val contentWidth = constraints.maxWidth - logicalContentXOffset // Measuring the main content with the calculated width val contentPlaceable = contentMeasurable.measure( @@ -163,15 +169,44 @@ fun JetLimeExtendedEvent( maxOf(contentHeight, additional.height) } ?: contentHeight - // Placing the measured composables in the layout - layout(constraints.maxWidth, layoutHeight) { - additionalContentPlaceable?.placeRelative(x = 0, y = 0) - contentPlaceable.placeRelative(x = contentXOffset.toInt(), y = 0) + val totalWidth = constraints.maxWidth + + // Placing the measured composables in the layout, mirroring in RTL so that + // additional content is always on the logical "left" of the timeline and main + // content on the "right", but visually flipped when isRtl is true. + layout(totalWidth, layoutHeight) { + if (isRtl) { + // In RTL, place additional content flush to the right so it stays visible + additionalContentPlaceable?.placeRelative( + x = totalWidth - additionalContentPlaceable.width, + y = 0, + ) + } else { + // LTR: original behavior, additional content starts from left + additionalContentPlaceable?.placeRelative(x = 0, y = 0) + } + + // Place main content on the opposite side of the timeline depending on direction + val contentX = if (isRtl) { + // In RTL, main content should be left of the timeline, but still within bounds + (logicalTimelineXOffset - contentPlaceable.width - jetLimeStyle.contentDistance.toPx()) + .coerceAtLeast(0f) + .toInt() + } else { + logicalContentXOffset.toInt() + } + contentPlaceable.placeRelative(x = contentX, y = 0) } } // Drawing on canvas for additional graphical elements Canvas(modifier = Modifier.matchParentSize()) { + // Use the logical timeline offset directly in both LTR and RTL so that + // the line stays aligned with the layout’s coordinate system. RTL + // placement is handled by how content is positioned relative to this + // logical offset, avoiding overlap with the main content. + val timelineXOffset = logicalTimelineXOffset + val yOffset = when (style.pointPlacement) { PointPlacement.START -> style.pointRadius.toPx() * jetLimeStyle.pointStartFactor PointPlacement.CENTER -> ( @@ -179,6 +214,7 @@ fun JetLimeExtendedEvent( if (style.position.isNotEnd()) jetLimeStyle.itemSpacing.toPx() else 0f ) / 2f + PointPlacement.END -> { val effectiveHeight = this.size.height - diff --git a/sample/composeApp/src/androidMain/kotlin/com/pushpal/jetlime/sample/JetLimePreviews.kt b/sample/composeApp/src/androidMain/kotlin/com/pushpal/jetlime/sample/JetLimePreviews.kt new file mode 100644 index 0000000..1e33221 --- /dev/null +++ b/sample/composeApp/src/androidMain/kotlin/com/pushpal/jetlime/sample/JetLimePreviews.kt @@ -0,0 +1,158 @@ +/* +* MIT License +* +* Copyright (c) 2024 Pushpal Roy +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +package com.pushpal.jetlime.sample + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ExperimentalComposeApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.pushpal.jetlime.ItemsList +import com.pushpal.jetlime.JetLimeColumn +import com.pushpal.jetlime.JetLimeDefaults +import com.pushpal.jetlime.JetLimeEvent +import com.pushpal.jetlime.JetLimeEventDefaults +import com.pushpal.jetlime.JetLimeExtendedEvent +import com.pushpal.jetlime.JetLimeRow +import com.pushpal.jetlime.PointPlacement + +@Preview(name = "JetLimeEvent Horizontal LTR", showBackground = true, widthDp = 360) +@Composable +private fun JetLimeEventHorizontalLtrPreview() { + MaterialTheme { + Surface { + Row(modifier = Modifier.padding(16.dp)) { + JetLimeRow( + itemsList = ItemsList(listOf("One", "Two", "Three")), + style = JetLimeDefaults.rowStyle(), + ) { index, item, position -> + JetLimeEvent( + style = JetLimeEventDefaults.eventStyle( + position = position, + pointPlacement = when (index) { + 0 -> PointPlacement.START + 1 -> PointPlacement.CENTER + else -> PointPlacement.END + }, + ), + ) { + Text(text = item, modifier = Modifier.padding(8.dp)) + } + } + } + } + } +} + +@Preview(name = "JetLimeEvent Horizontal RTL", showBackground = true, widthDp = 360) +@Composable +private fun JetLimeEventHorizontalRtlPreview() { + MaterialTheme { + Surface { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + Row(modifier = Modifier.padding(16.dp)) { + JetLimeRow( + itemsList = ItemsList(listOf("One", "Two", "Three")), + style = JetLimeDefaults.rowStyle(), + ) { index, item, position -> + JetLimeEvent( + style = JetLimeEventDefaults.eventStyle( + position = position, + pointPlacement = when (index) { + 0 -> PointPlacement.START + 1 -> PointPlacement.CENTER + else -> PointPlacement.END + }, + ), + ) { + Text(text = item, modifier = Modifier.padding(8.dp)) + } + } + } + } + } + } +} + +@OptIn(ExperimentalComposeApi::class) +@Preview(name = "JetLimeExtendedEvent LTR", showBackground = true, widthDp = 360) +@Composable +private fun JetLimeExtendedEventLtrPreview() { + MaterialTheme { + Surface { + Column(modifier = Modifier.padding(16.dp)) { + JetLimeColumn( + itemsList = ItemsList(listOf("A", "B", "C")), + style = JetLimeDefaults.columnStyle(), + ) { index, item, position -> + JetLimeExtendedEvent( + style = JetLimeEventDefaults.eventStyle(position = position), + additionalContent = { + Text(text = "Left $index", modifier = Modifier.padding(4.dp)) + }, + ) { + Text(text = "Right $item", modifier = Modifier.padding(4.dp)) + } + } + } + } + } +} + +@OptIn(ExperimentalComposeApi::class) +@Preview(name = "JetLimeExtendedEvent RTL", showBackground = true, widthDp = 360) +@Composable +private fun JetLimeExtendedEventRtlPreview() { + MaterialTheme { + Surface { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + Column(modifier = Modifier.padding(16.dp)) { + JetLimeColumn( + itemsList = ItemsList(listOf("A", "B", "C")), + style = JetLimeDefaults.columnStyle(), + ) { index, item, position -> + JetLimeExtendedEvent( + style = JetLimeEventDefaults.eventStyle(position = position), + additionalContent = { + Text(text = "Left $index", modifier = Modifier.padding(4.dp)) + }, + ) { + Text(text = "Right $item", modifier = Modifier.padding(4.dp)) + } + } + } + } + } + } +}