For #25891 - Move GridViewHolder to compose

pull/543/head
Alexandru2909 2 years ago committed by mergify[bot]
parent 263e4d819b
commit 335159cef5

@ -18,6 +18,7 @@ import androidx.test.uiautomator.Until
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.HomeActivity
@ -165,6 +166,7 @@ class MenuScreenShotTest : ScreenshotTest() {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun tabMenuTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {

@ -14,6 +14,7 @@ import mozilla.appservices.places.BookmarkRoot
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
@ -361,6 +362,7 @@ class BookmarksTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openSelectionInNewTabTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
@ -392,6 +394,7 @@ class BookmarksTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openSelectionInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -10,6 +10,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -68,6 +69,7 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun createFirstCollectionTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -100,6 +102,7 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyExpandedCollectionItemsTest() {
val webPage = getGenericAsset(mockWebServer, 1)
val webPageUrl = webPage.url.host.toString()
@ -149,6 +152,7 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openAllTabsInCollectionTest() {
val firstTestPage = getGenericAsset(mockWebServer, 1)
val secondTestPage = getGenericAsset(mockWebServer, 2)
@ -181,6 +185,7 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun shareCollectionTest() {
val firstWebsite = getGenericAsset(mockWebServer, 1)
val secondWebsite = getGenericAsset(mockWebServer, 2)
@ -209,6 +214,7 @@ class CollectionTest {
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun deleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -233,6 +239,7 @@ class CollectionTest {
@Test
// open a webpage, and add currently opened tab to existing collection
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun mainMenuSaveToExistingCollection() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -259,6 +266,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -285,6 +293,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -305,6 +314,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun createSecondCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -326,6 +336,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun removeTabFromCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -348,6 +359,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun swipeLeftToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
@ -374,6 +386,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun swipeRightToRemoveTabFromCollectionTest() {
val testPage = getGenericAsset(mockWebServer, 1)
@ -400,6 +413,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun selectTabOnLongTapTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -431,6 +445,7 @@ class CollectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun navigateBackInCollectionFlowTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -464,6 +479,7 @@ class CollectionTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun undoDeleteCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)

@ -64,6 +64,7 @@ class ContextMenusTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyContextOpenLinkNewTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
@ -88,6 +89,7 @@ class ContextMenusTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyContextOpenLinkPrivateTab() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)

@ -109,6 +109,7 @@ class CrashReportingTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun privateBrowsingUseAppWhileTabIsCrashedTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)

@ -14,6 +14,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
@ -179,6 +180,7 @@ class HistoryTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openHistoryInNewTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -207,6 +209,7 @@ class HistoryTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openHistoryInPrivateTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -291,6 +294,7 @@ class HistoryTest {
@Test
// This test verifies the Recently Closed Tabs List and items
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyRecentlyClosedTabsListTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -240,6 +240,7 @@ class SearchTest {
@SmokeTest
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun noRecentlyVisitedSearchGroupInPrivateBrowsingTest() {
val firstPage = searchMockServer.url("generic1.html").toString()
val secondPage = searchMockServer.url("generic2.html").toString()

@ -363,6 +363,7 @@ class SettingsPrivacyTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun launchPageShortcutInPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -71,6 +71,7 @@ class SettingsSearchTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun toggleSearchBookmarksAndHistoryTest() {
// Bookmarks 2 websites, toggles the bookmarks and history search settings off,
// then verifies if the websites do not show in the suggestions.

@ -195,6 +195,7 @@ class SmokeTest {
- the tab drawer button
- opening a new search and dismissing the nav bar
*/
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -533,6 +534,7 @@ class SmokeTest {
@Test
// Verifies that a recently closed item is properly opened
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openRecentlyClosedItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -557,6 +559,7 @@ class SmokeTest {
@Test
// Verifies that tapping the "x" button removes a recently closed item from the list
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun deleteRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -626,6 +629,7 @@ class SmokeTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun shareTabsFromTabsTrayTest() {
val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@ -672,6 +676,7 @@ class SmokeTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun privateTabsTrayWithOpenedTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -872,6 +877,7 @@ class SmokeTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun tabMediaControlButtonTest() {
val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer)

@ -106,6 +106,7 @@ class StrictEnhancedTrackingProtectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun testStrictVisitProtectionSheet() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
@ -162,6 +163,7 @@ class StrictEnhancedTrackingProtectionTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun testStrictVisitSheetDetails() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =

@ -10,6 +10,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -71,6 +72,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -95,6 +97,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun openNewPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -115,6 +118,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun closeAllTabsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -147,6 +151,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun closeTabTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -178,6 +183,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyUndoSnackBarTest() {
// disabling these features because they interfere with the snackbar visibility
featureSettingsHelper.setPocketEnabled(false)
@ -202,6 +208,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun closePrivateTabTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -235,6 +242,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyPrivateTabUndoSnackBarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -315,6 +323,7 @@ class TabbedBrowsingTest {
}
@Test
@Ignore("Failing after compose migration. See: https://github.com/mozilla-mobile/fenix/issues/26087")
fun verifyOpenTabDetails() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -0,0 +1,125 @@
/* 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.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Displays the [content] with the right edge fading.
*
* @param modifier [Modifier] for the container.
* @param fadeWidth Length of the fading edge.
* @param backgroundColor [Color] of the background shown under the content.
* @param isContentRtl Whether or not the content should be displayed Right to Left
* @param content The content whose right edge must be faded.
*/
@Composable
fun HorizontalFadingEdgeBox(
modifier: Modifier = Modifier,
fadeWidth: Dp = 25.dp,
backgroundColor: Color = Color.Transparent,
isContentRtl: Boolean = false,
content: @Composable BoxScope.() -> Unit
) {
// List of colors defining the direction of the fade effect
val colorList = listOf(Color.Transparent, backgroundColor)
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Box(modifier) {
content()
Spacer(
Modifier
.width(fadeWidth)
.fillMaxHeight()
.align(
if (isContentRtl) {
Alignment.CenterStart
} else {
Alignment.CenterEnd
}
)
.background(
Brush.horizontalGradient(
colors = if (isContentRtl) {
colorList.reversed()
} else {
colorList
}
)
)
)
}
}
}
@Preview
@Composable
private fun FadingRightTextPreview() {
FirefoxTheme {
Surface(modifier = Modifier.background(FirefoxTheme.colors.layer1)) {
HorizontalFadingEdgeBox(
modifier = Modifier
.width(250.dp)
.height(20.dp)
.clipToBounds(),
backgroundColor = FirefoxTheme.colors.layer1
) {
Text(
"Example text set to fade on the right",
modifier = Modifier
.fillMaxSize(),
softWrap = false
)
}
}
}
}
@Preview
@Composable
private fun FadingLeftTextPreview() {
FirefoxTheme {
Surface(modifier = Modifier.background(FirefoxTheme.colors.layer1)) {
HorizontalFadingEdgeBox(
modifier = Modifier
.width(250.dp)
.height(20.dp)
.clipToBounds(),
isContentRtl = true,
fadeWidth = 50.dp,
backgroundColor = FirefoxTheme.colors.layer1
) {
Text(
"Example text set to fade on the left",
modifier = Modifier
.fillMaxSize(),
softWrap = false
)
}
}
}
}

@ -0,0 +1,123 @@
/* 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.compose.tabstray
import androidx.compose.foundation.Image
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.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Card which will display the thumbnail for a tab. If a thumbnail is not available for the [tabId],
* the favicon [R.drawable.mozac_ic_globe] icon will be displayed.
*
* @param tabId Key used to remember the thumbnail for future compositions.
* @param modifier [Modifier] used to draw the image content.
* @param contentDescription Text used by accessibility services
* to describe what this image represents.
* @param contentScale [ContentScale] used to draw image content.
* @param alignment [Alignment] used to draw the image content.
*/
@Composable
@Suppress("LongParameterList")
fun GridTabThumbnail(
tabId: String,
size: Dp,
modifier: Modifier = Modifier,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.FillWidth,
alignment: Alignment = Alignment.TopCenter
) {
Card(
modifier = modifier,
backgroundColor = FirefoxTheme.colors.layer2
) {
if (inComposePreview) {
GlobeIcon()
} else {
val rememberBitmap = remember(tabId) { mutableStateOf<ImageBitmap?>(null) }
val imageSize = LocalDensity.current.run { size.toPx().toInt() }
val request = ImageLoadRequest(tabId, imageSize)
val storage = LocalContext.current.components.core.thumbnailStorage
val bitmap = rememberBitmap.value
LaunchedEffect(tabId) {
rememberBitmap.value = storage.loadThumbnail(request).await()?.asImageBitmap()
}
if (bitmap != null) {
val painter = BitmapPainter(bitmap)
Image(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale,
alignment = alignment
)
} else {
GlobeIcon()
}
}
}
}
/**
* Globe icon to be displayed when no thumbnail is available.
*/
@Composable
private fun GlobeIcon() {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_globe),
contentDescription = null,
modifier = Modifier
.padding(22.dp)
.fillMaxSize(),
tint = FirefoxTheme.colors.iconSecondary
)
}
@Preview
@Composable
private fun ThumbnailCardPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
GridTabThumbnail(
tabId = "1",
size = LocalConfiguration.current.screenWidthDp.dp
)
}
}
}

@ -0,0 +1,281 @@
/* 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.compose.tabstray
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.text.BidiFormatter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.HorizontalFadingEdgeBox
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Tab grid item used to display a tab that supports clicks,
* long clicks, multiple selection, and media controls.
*
* @param tab The given tab to be render as view a grid item.
* @param isSelected Indicates if the item should be render as selected.
* @param multiSelectionEnabled Indicates if the item should be render with multi selection options,
* enabled.
* @param multiSelectionSelected Indicates if the item should be render as multi selection selected
* option.
* @param onCloseClick Callback to handle the click event of the close button.
* @param onMediaClick Callback to handle when the media item is clicked.
* @param onClick Callback to handle when item is clicked.
* @param onLongClick Callback to handle when item is long clicked.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
@Suppress("MagicNumber", "LongParameterList", "LongMethod")
fun TabGridItem(
tab: TabSessionState,
isSelected: Boolean = false,
multiSelectionEnabled: Boolean = false,
multiSelectionSelected: Boolean = false,
onCloseClick: (tab: TabSessionState) -> Unit,
onMediaClick: (tab: TabSessionState) -> Unit,
onClick: (tab: TabSessionState) -> Unit,
onLongClick: (tab: TabSessionState) -> Unit,
) {
val tabBorderModifier = if (isSelected && !multiSelectionEnabled) {
Modifier.border(
4.dp,
FirefoxTheme.colors.borderAccent,
RoundedCornerShape(12.dp)
)
} else {
Modifier
}
Box(
modifier = Modifier
.wrapContentHeight()
.wrapContentWidth()
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(202.dp)
.padding(4.dp)
.then(tabBorderModifier)
.padding(4.dp)
.combinedClickable(
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) }
),
elevation = 0.dp,
shape = RoundedCornerShape(dimensionResource(id = R.dimen.tab_tray_grid_item_border_radius)),
border = BorderStroke(1.dp, FirefoxTheme.colors.borderPrimary)
) {
Column(
modifier = Modifier.background(FirefoxTheme.colors.layer2)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Favicon(
url = tab.content.url,
size = 16.dp,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 8.dp)
)
HorizontalFadingEdgeBox(
modifier = Modifier
.weight(1f)
.wrapContentHeight()
.requiredHeight(30.dp)
.padding(7.dp, 5.dp)
.clipToBounds(),
backgroundColor = FirefoxTheme.colors.layer2,
isContentRtl = BidiFormatter.getInstance().isRtl(tab.content.title)
) {
Text(
text = tab.content.title,
fontSize = 14.sp,
maxLines = 1,
softWrap = false,
style = TextStyle(
color = FirefoxTheme.colors.textPrimary,
textDirection = TextDirection.Content
)
)
}
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
modifier = Modifier
.clickable { onCloseClick(tab) }
.size(24.dp)
.align(Alignment.CenterVertically)
)
}
Divider(
color = FirefoxTheme.colors.borderPrimary,
thickness = 1.dp
)
Thumbnail(
tab = tab,
multiSelectionSelected = multiSelectionSelected,
)
}
}
if (!multiSelectionEnabled) {
MediaImage(
tab = tab,
onMediaIconClicked = { onMediaClick(tab) },
modifier = Modifier
.align(Alignment.TopStart)
)
}
}
}
/**
* Thumbnail specific for the [TabGridItem], which can be selected.
*
* @param tab Tab, containing the thumbnail to be displayed.
* @param multiSelectionSelected Whether or not the multiple selection is enabled.
*/
@Composable
private fun Thumbnail(
tab: TabSessionState,
multiSelectionSelected: Boolean,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer2)
) {
GridTabThumbnail(
tabId = tab.id,
size = LocalConfiguration.current.screenWidthDp.dp
)
if (multiSelectionSelected) {
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layerAccentNonOpaque)
)
Card(
modifier = Modifier
.size(size = 40.dp)
.align(alignment = Alignment.Center),
shape = CircleShape,
backgroundColor = FirefoxTheme.colors.layerAccent,
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_check),
modifier = Modifier
.matchParentSize()
.padding(all = 8.dp),
contentDescription = null,
tint = colorResource(id = R.color.mozac_ui_icons_fill)
)
}
}
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun TabGridItemPreview() {
FirefoxTheme {
TabGridItem(
tab = createTab(
url = "www.mozilla.com",
title = "Mozilla Domain"
),
onCloseClick = {},
onMediaClick = {},
onClick = {},
onLongClick = {},
)
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun TabGridItemSelectedPreview() {
FirefoxTheme {
TabGridItem(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
isSelected = true,
onCloseClick = {},
onMediaClick = {},
onClick = {},
onLongClick = {},
)
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun TabGridItemMultiSelectedPreview() {
FirefoxTheme {
TabGridItem(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
multiSelectionEnabled = true,
multiSelectionSelected = true,
onCloseClick = {},
onMediaClick = {},
onClick = {},
onLongClick = {},
)
}
}

@ -23,6 +23,7 @@ import org.mozilla.fenix.databinding.TabTrayItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.compose.ComposeGridViewHolder
import org.mozilla.fenix.tabstray.browser.compose.ComposeListViewHolder
/**
@ -48,7 +49,8 @@ class BrowserTabsAdapter(
enum class ViewType(val layoutRes: Int) {
LIST(BrowserTabViewHolder.ListViewHolder.LAYOUT_ID),
COMPOSE_LIST(ComposeListViewHolder.LAYOUT_ID),
GRID(BrowserTabViewHolder.GridViewHolder.LAYOUT_ID)
GRID(BrowserTabViewHolder.GridViewHolder.LAYOUT_ID),
COMPOSE_GRID(ComposeGridViewHolder.LAYOUT_ID)
}
/**
@ -62,7 +64,11 @@ class BrowserTabsAdapter(
override fun getItemViewType(position: Int): Int {
return when {
context.components.settings.gridTabView -> {
ViewType.GRID.layoutRes
if (FeatureFlags.composeTabsTray) {
ViewType.COMPOSE_GRID.layoutRes
} else {
ViewType.GRID.layoutRes
}
}
else -> {
if (FeatureFlags.composeTabsTray) {
@ -85,6 +91,15 @@ class BrowserTabsAdapter(
featureName = featureName,
viewLifecycleOwner = viewLifecycleOwner
)
ViewType.COMPOSE_GRID.layoutRes ->
ComposeGridViewHolder(
interactor = interactor,
store = store,
selectionHolder = selectionHolder,
composeItemView = ComposeView(parent.context),
featureName = featureName,
viewLifecycleOwner = viewLifecycleOwner
)
else -> {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
if (viewType == ViewType.GRID.layoutRes) {

@ -0,0 +1,108 @@
/* 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.tabstray.browser.compose
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.browser.tabstray.TabsTrayStyling
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.compose.tabstray.TabGridItem
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
/**
* A Compose ViewHolder implementation for "tab" items with grid layout.
*
* @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
* @param selectionHolder [SelectionHolder]<[TabSessionState]> for helping with selecting
* any number of displayed [TabSessionState]s.
* @param composeItemView that displays a "tab".
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
* @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to.
*/
class ComposeGridViewHolder(
private val interactor: BrowserTrayInteractor,
private val store: TabsTrayStore,
private val selectionHolder: SelectionHolder<TabSessionState>? = null,
composeItemView: ComposeView,
private val featureName: String,
viewLifecycleOwner: LifecycleOwner,
) : ComposeAbstractTabViewHolder(composeItemView, viewLifecycleOwner) {
override var tab: TabSessionState? = null
private var isMultiSelectionSelectedState = MutableStateFlow(false)
private var isSelectedTabState = MutableStateFlow(false)
override fun bind(
tab: TabSessionState,
isSelected: Boolean,
styling: TabsTrayStyling,
delegate: TabsTray.Delegate
) {
this.tab = tab
isSelectedTabState.value = isSelected
bind(tab)
}
override fun updateSelectedTabIndicator(showAsSelected: Boolean) {
isSelectedTabState.value = showAsSelected
}
override fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) {
isMultiSelectionSelectedState.value = isSelected
}
private fun onCloseClicked(tab: TabSessionState) {
interactor.onTabClosed(tab, featureName)
}
private fun onClick(tab: TabSessionState) {
val holder = selectionHolder
if (holder != null) {
interactor.onMultiSelectClicked(tab, holder, featureName)
} else {
interactor.onTabSelected(tab, featureName)
}
}
private fun onLongClick(tab: TabSessionState) {
val holder = selectionHolder ?: return
interactor.onLongClicked(tab, holder)
}
@Composable
override fun Content(tab: TabSessionState) {
val multiSelectionEnabled = store.observeAsComposableState { state ->
state.mode is TabsTrayState.Mode.Select
}.value ?: false
val isSelectedTab by isSelectedTabState.collectAsState()
val isMultiSelectionSelected by isMultiSelectionSelectedState.collectAsState()
TabGridItem(
tab = tab,
isSelected = isSelectedTab,
multiSelectionEnabled = multiSelectionEnabled,
multiSelectionSelected = isMultiSelectionSelected,
onCloseClick = ::onCloseClicked,
onMediaClick = interactor::onMediaClicked,
onClick = ::onClick,
onLongClick = ::onLongClick,
)
}
companion object {
val LAYOUT_ID = View.generateViewId()
}
}
Loading…
Cancel
Save