diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt index 8945e45a38..ead53ab533 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt @@ -12,29 +12,39 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.unit.dp import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import kotlinx.coroutines.launch +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.TabSessionState import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.compose.Divider import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme /** * Top-level UI for displaying the Tabs Tray feature. * * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState]. + * @param displayTabsInGrid Whether the normal and private tabs should be displayed in a grid. */ -@OptIn(ExperimentalPagerApi::class) +@OptIn(ExperimentalPagerApi::class, ExperimentalComposeUiApi::class) @Composable fun TabsTray( tabsTrayStore: TabsTrayStore, + displayTabsInGrid: Boolean, ) { val multiselectMode = tabsTrayStore .observeAsComposableState { state -> state.mode }.value ?: TabsTrayState.Mode.Normal + val normalTabs = tabsTrayStore + .observeAsComposableState { state -> state.normalTabs }.value ?: emptyList() val pagerState = rememberPagerState(initialPage = 0) val scope = rememberCoroutineScope() val animateScrollToPage: ((Page) -> Unit) = { page -> @@ -48,10 +58,12 @@ fun TabsTray( .fillMaxSize() .background(FirefoxTheme.colors.layer1), ) { - TabsTrayBanner( - isInMultiSelectMode = multiselectMode is TabsTrayState.Mode.Select, - onTabPageIndicatorClicked = animateScrollToPage, - ) + Box(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) { + TabsTrayBanner( + isInMultiSelectMode = multiselectMode is TabsTrayState.Mode.Select, + onTabPageIndicatorClicked = animateScrollToPage, + ) + } Divider() @@ -64,12 +76,17 @@ fun TabsTray( ) { position -> when (Page.positionToPage(position)) { Page.NormalTabs -> { - Text( - text = "Normal tabs", - modifier = Modifier.padding(all = 16.dp), - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.body1, - ) + FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) { + if (displayTabsInGrid) { + TabGrid( + tabs = normalTabs, + ) + } else { + TabList( + tabs = normalTabs, + ) + } + } } Page.PrivateTabs -> { Text( @@ -96,9 +113,16 @@ fun TabsTray( @LightDarkPreview @Composable private fun TabsTrayPreview() { + val store = TabsTrayStore( + initialState = TabsTrayState( + normalTabs = generateFakeTabsList(), + ), + ) + FirefoxTheme { TabsTray( - tabsTrayStore = TabsTrayStore(), + tabsTrayStore = store, + displayTabsInGrid = false, ) } } @@ -109,12 +133,25 @@ private fun TabsTrayMultiSelectPreview() { val store = TabsTrayStore( initialState = TabsTrayState( mode = TabsTrayState.Mode.Select(setOf()), + normalTabs = generateFakeTabsList(), ), ) FirefoxTheme { TabsTray( tabsTrayStore = store, + displayTabsInGrid = true, ) } } + +private fun generateFakeTabsList(tabCount: Int = 10): List { + val fakeTab = TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ), + ) + + return List(tabCount) { fakeTab } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 52eedd878c..14ba8594aa 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -208,6 +208,7 @@ class TabsTrayFragment : AppCompatDialogFragment() { FirefoxTheme { TabsTray( tabsTrayStore = tabsTrayStore, + displayTabsInGrid = requireContext().settings().gridTabView, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayTabLayouts.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayTabLayouts.kt new file mode 100644 index 0000000000..f21808d2c1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayTabLayouts.kt @@ -0,0 +1,145 @@ +/* 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 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.TabSessionState +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.tabstray.TabGridItem +import org.mozilla.fenix.compose.tabstray.TabListItem +import org.mozilla.fenix.tabstray.ext.MIN_COLUMN_WIDTH_DP +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Top-level UI for displaying a list of tabs. + * + * @param tabs The list of [TabSessionState] to display. + */ +@Composable +fun TabList( + tabs: List, +) { + val tabListBottomPadding = dimensionResource(id = R.dimen.tab_tray_list_bottom_padding) + val state = rememberLazyListState() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = state, + ) { + items( + items = tabs, + key = { tab -> tab.id }, + ) { tab -> + TabListItem( + tab = tab, + onCloseClick = {}, + onMediaClick = {}, + onClick = {}, + onLongClick = {}, + ) + } + + item { + Spacer(modifier = Modifier.height(tabListBottomPadding)) + } + } +} + +/** + * Top-level UI for displaying a grid of tabs. + * + * @param tabs The list of [TabSessionState] to display. + */ +@Composable +fun TabGrid( + tabs: List, +) { + val tabListBottomPadding = dimensionResource(id = R.dimen.tab_tray_list_bottom_padding) + val state = rememberLazyGridState() + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = MIN_COLUMN_WIDTH_DP.dp), + modifier = Modifier.fillMaxSize(), + state = state, + ) { + items( + items = tabs, + key = { tab -> tab.id }, + ) { tab -> + TabGridItem( + tab = tab, + onCloseClick = {}, + onMediaClick = {}, + onClick = {}, + onLongClick = {}, + ) + } + + item(span = { GridItemSpan(maxLineSpan) }) { + Spacer(modifier = Modifier.height(tabListBottomPadding)) + } + } +} + +@LightDarkPreview +@Composable +private fun TabListPreview() { + FirefoxTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(FirefoxTheme.colors.layer1), + ) { + TabList( + tabs = generateFakeTabsList(), + ) + } + } +} + +@LightDarkPreview +@Composable +private fun TabGridPreview() { + FirefoxTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(FirefoxTheme.colors.layer1), + ) { + TabGrid( + tabs = generateFakeTabsList(), + ) + } + } +} + +private fun generateFakeTabsList(tabCount: Int = 10): List { + val fakeTab = TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ), + ) + + return List(tabCount) { fakeTab } +}