You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt

239 lines
8.5 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.collections
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissDirection.EndToStart
import androidx.compose.material.DismissDirection.StartToEnd
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.Tab
import org.mozilla.fenix.R.drawable
import org.mozilla.fenix.R.string
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.list.FaviconListItem
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Rectangular shape with only right angles used to display a middle tab.
*/
private val MIDDLE_TAB_SHAPE = RoundedCornerShape(0.dp)
/**
* Rectangular shape with only the bottom corners rounded used to display the last tab in a collection.
*/
private val BOTTOM_TAB_SHAPE = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)
/**
* Display an individual [Tab] as part of a collection.
*
* @param tab [Tab] to display.
* @param isLastInCollection Whether the tab is to be shown between others or as the last one in collection.
* @param onClick Invoked when the user click on the tab.
* @param onRemove Invoked when the user removes the tab informing also if the tab was swiped to be removed.
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CollectionItem(
tab: Tab,
isLastInCollection: Boolean,
onClick: () -> Unit,
onRemove: (Boolean) -> Unit,
) {
val dismissState = rememberDismissState()
if (dismissState.isDismissed(StartToEnd) || dismissState.isDismissed(EndToStart)) {
onRemove(true)
}
SwipeToDismiss(
state = dismissState,
background = {
DismissedTabBackground(
dismissDirection = dismissState.dismissDirection,
isLastInCollection = isLastInCollection,
)
},
) {
// We need to clip the top bounds to avoid this item drawing shadows over the above item.
// But we need to add this shadows back to have a clearer separation between tabs
// when one is being swiped away.
val clippingModifier by remember {
derivedStateOf {
try {
if (dismissState.progress.fraction != 1f) Modifier else Modifier.clipTop()
} catch (e: NoSuchElementException) {
// `androidx.compose.material.Swipeable.findBounds` couldn't find anchors.
// Happened once in testing when deleting a tab. Could not reproduce afterwards.
Modifier.clipTop()
}
}
}
Card(
modifier = clippingModifier
.fillMaxWidth(),
shape = if (isLastInCollection) BOTTOM_TAB_SHAPE else MIDDLE_TAB_SHAPE,
backgroundColor = FirefoxTheme.colors.layer2,
elevation = 5.dp,
) {
FaviconListItem(
label = tab.title,
description = tab.url.toShortUrl(),
onClick = onClick,
url = tab.url,
iconPainter = painterResource(drawable.ic_close),
iconDescription = stringResource(string.remove_tab_from_collection),
onIconClick = { onRemove(false) },
)
}
}
}
/**
* Composable used to display the background of a [Tab] shown in collections that is being swiped left or right.
*
* @param dismissDirection [DismissDirection] of the tab being swiped depending on which this composable
* will also indicate the swipe direction by placing a warning icon at the start of the swipe gesture.
* If `null` the warning icon will be shown at both ends.
* @param isLastInCollection Whether the tab is to be shown between others or as the last one in collection.
*/
@Composable
private fun DismissedTabBackground(
dismissDirection: DismissDirection?,
isLastInCollection: Boolean,
) {
Card(
modifier = Modifier.fillMaxSize(),
backgroundColor = FirefoxTheme.colors.layer3,
shape = if (isLastInCollection) BOTTOM_TAB_SHAPE else MIDDLE_TAB_SHAPE,
elevation = 0.dp,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(drawable.ic_delete),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 32.dp)
// Only show the delete icon for where the swipe starts.
.alpha(
if (dismissDirection == StartToEnd) 1f else 0f,
),
tint = FirefoxTheme.colors.iconWarning,
)
Icon(
painter = painterResource(drawable.ic_delete),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 32.dp)
// Only show the delete icon for where the swipe starts.
.alpha(
if (dismissDirection == EndToStart) 1f else 0f,
),
tint = FirefoxTheme.colors.iconWarning,
)
}
}
}
/**
* Clips the Composable this applies to such that it cannot draw content / shadows outside it's top bound.
*/
private fun Modifier.clipTop() = this.then(
Modifier.drawWithContent {
val paddingPx = Constraints.Infinity.toFloat()
clipRect(
left = 0f - paddingPx,
top = 0f,
right = size.width + paddingPx,
bottom = size.height + paddingPx,
) {
this@drawWithContent.drawContent()
}
},
)
@Composable
@LightDarkPreview
private fun TabInCollectionPreview() {
FirefoxTheme {
Column {
Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground(
dismissDirection = StartToEnd,
isLastInCollection = false,
)
}
CollectionItem(
tab = tabPreview,
isLastInCollection = false,
onClick = {},
onRemove = {},
)
Spacer(Modifier.height(10.dp))
Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground(
dismissDirection = EndToStart,
isLastInCollection = true,
)
}
CollectionItem(
tab = tabPreview,
isLastInCollection = true,
onClick = {},
onRemove = {},
)
}
}
}
private val tabPreview = object : Tab {
override val id = 2L
override val title = "Mozilla-Firefox"
override val url = "https://www.mozilla.org/en-US/firefox/whats-new-in-last-version"
override fun restore(
context: Context,
engine: Engine,
restoreSessionId: Boolean,
): RecoverableTab? = null
}