Bug 1940636 - Add an initial custom tab toolbar implementation r=android-reviewers,jonalmeida

Differential Revision: https://phabricator.services.mozilla.com/D233612
This commit is contained in:
Gabriel Luong
2025-02-19 03:24:30 +00:00
parent d399af8eef
commit 9905e0b47c
11 changed files with 309 additions and 33 deletions

View File

@@ -10,6 +10,7 @@ import mozilla.components.browser.state.helper.Target
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarStore
import mozilla.components.compose.browser.toolbar.store.Mode
import mozilla.components.lib.state.ext.observeAsState
/**
@@ -52,7 +53,8 @@ fun BrowserToolbar(
else -> editText
}
if (uiState.editMode) {
when (uiState.mode) {
Mode.EDIT -> {
BrowserEditToolbar(
url = input,
colors = colors.editToolbarColors,
@@ -61,7 +63,9 @@ fun BrowserToolbar(
onUrlCommitted = { text -> onTextCommit(text) },
onUrlEdit = { text -> onTextEdit(text) },
)
} else {
}
Mode.DISPLAY -> {
BrowserDisplayToolbar(
url = selectedTab?.content?.url ?: uiState.displayState.hint,
colors = colors.displayToolbarColors,
@@ -73,4 +77,16 @@ fun BrowserToolbar(
},
)
}
Mode.CUSTOM_TAB -> {
CustomTabToolbar(
url = selectedTab?.content?.url ?: "",
title = selectedTab?.content?.title ?: "",
colors = colors.customTabToolbarColor,
navigationActions = uiState.displayState.navigationActions,
pageActions = uiState.displayState.pageActions,
browserActions = uiState.displayState.browserActions,
)
}
}
}

View File

@@ -9,14 +9,29 @@ import androidx.compose.ui.graphics.Color
/**
* Represents the colors used by the browser toolbar.
*
* @property customTabToolbarColor The color scheme to use in the custom tab toolbar.
* @property displayToolbarColors The color scheme to use in browser display toolbar.
* @property editToolbarColors The color scheme to use in the browser edit toolbar.
*/
data class BrowserToolbarColors(
val customTabToolbarColor: CustomTabToolbarColors,
val displayToolbarColors: BrowserDisplayToolbarColors,
val editToolbarColors: BrowserEditToolbarColors,
)
/**
* Represents the colors used by the custom tab toolbar.
*
* @property background Background color of the toolbar.
* @property title Text color of the title.
* @property url Text color of the URL.
*/
data class CustomTabToolbarColors(
val background: Color,
val title: Color,
val url: Color,
)
/**
* Represents the colors used by the browser display toolbar.
*

View File

@@ -18,6 +18,11 @@ object BrowserToolbarDefaults {
*/
@Composable
fun colors(
customTabToolbarColors: CustomTabToolbarColors = CustomTabToolbarColors(
background = AcornTheme.colors.layer1,
title = AcornTheme.colors.textPrimary,
url = AcornTheme.colors.textPrimary,
),
displayToolbarColors: BrowserDisplayToolbarColors = BrowserDisplayToolbarColors(
background = AcornTheme.colors.layer1,
urlBackground = AcornTheme.colors.layer3,
@@ -30,6 +35,7 @@ object BrowserToolbarDefaults {
clearButton = AcornTheme.colors.iconPrimary,
),
) = BrowserToolbarColors(
customTabToolbarColor = customTabToolbarColors,
displayToolbarColors = displayToolbarColors,
editToolbarColors = editToolbarColors,
)

View File

@@ -0,0 +1,116 @@
/* 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 mozilla.components.compose.browser.toolbar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.compose.browser.toolbar.concept.Action
import mozilla.components.ui.icons.R as iconsR
/**
* Sub-component of the [BrowserToolbar] responsible for displaying the custom tab.
*
* @param url The URL to be displayed.
* @param title The title to be displayed.
* @param colors The color scheme to use in the custom tab toolbar.
* @param navigationActions List of navigation [Action]s to be displayed on left side of the
* display toolbar (outside of the URL bounding box).
* @param pageActions List of page [Action]s to be displayed to the right side of the URL of the
* display toolbar. Also see:
* [MDN docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction)
* @param browserActions List of browser [Action]s to be displayed on the right side of the
* display toolbar (outside of the URL bounding box). Also see:
* [MDN docs](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action)
*/
@Composable
fun CustomTabToolbar(
url: String,
title: String,
colors: CustomTabToolbarColors,
navigationActions: List<Action> = emptyList(),
pageActions: List<Action> = emptyList(),
browserActions: List<Action> = emptyList(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.background(color = colors.background),
verticalAlignment = Alignment.CenterVertically,
) {
ActionContainer(actions = navigationActions)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
) {
if (title.isNotEmpty()) {
Text(
text = title,
color = colors.title,
maxLines = 1,
style = AcornTheme.typography.headline8,
)
}
if (url.isNotEmpty()) {
Text(
text = url,
color = colors.url,
maxLines = 1,
style = AcornTheme.typography.caption,
)
}
}
ActionContainer(actions = pageActions)
ActionContainer(actions = browserActions)
}
}
@PreviewLightDark
@Composable
private fun CustomTabToolbarPreview() {
AcornTheme {
CustomTabToolbar(
url = "http://www.mozilla.org",
title = "Mozilla",
colors = CustomTabToolbarColors(
background = AcornTheme.colors.layer1,
title = AcornTheme.colors.textPrimary,
url = AcornTheme.colors.textSecondary,
),
navigationActions = listOf(
Action.ActionButton(
icon = iconsR.drawable.mozac_ic_cross_24,
contentDescription = null,
tint = AcornTheme.colors.iconPrimary.toArgb(),
onClick = {},
),
),
browserActions = listOf(
Action.ActionButton(
icon = iconsR.drawable.mozac_ic_arrow_clockwise_24,
contentDescription = null,
tint = AcornTheme.colors.iconPrimary.toArgb(),
onClick = {},
),
),
)
}
}

View File

@@ -10,15 +10,42 @@ import mozilla.components.lib.state.State
/**
* The state of the browser toolbar.
*
* @property mode The display [Mode] of the browser toolbar.
* @property displayState Wrapper containing the toolbar display state.
* @property editState Wrapper containing the toolbar edit state.
* @property editMode Whether the toolbar is in "edit" or "display" mode.
*/
data class BrowserToolbarState(
val mode: Mode = Mode.DISPLAY,
val displayState: DisplayState = DisplayState(),
val editState: EditState = EditState(),
val editMode: Boolean = false,
) : State
) : State {
/**
* Returns true if the browser toolbar is in edit mode and false otherwise.
*/
fun isEditMode() = this.mode == Mode.EDIT
}
/**
* The various display mode of the browser toolbar.
*/
enum class Mode {
/**
* Display mode - Shows the URL and related toolbar actions.
*/
DISPLAY,
/**
* Edit mode - Allows the user to edit the URL.
*/
EDIT,
/**
* Custom tab - Displays the URL and title of a custom tab.
*/
CUSTOM_TAB,
}
/**
* Wrapper containing the toolbar display state.

View File

@@ -19,7 +19,7 @@ class BrowserToolbarStore(
private fun reduce(state: BrowserToolbarState, action: BrowserToolbarAction): BrowserToolbarState {
return when (action) {
is BrowserToolbarAction.ToggleEditMode -> state.copy(
editMode = action.editMode,
mode = if (action.editMode) Mode.EDIT else Mode.DISPLAY,
editState = state.editState.copy(
editText = if (action.editMode) null else state.editState.editText,
),

View File

@@ -9,9 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -24,15 +22,15 @@ class BrowserToolbarStoreTest {
val coroutineTestRule = MainCoroutineRule()
@Test
fun `WHEN toggle edit mode action is dispatched THEN update the edit mode and states`() {
fun `WHEN toggle edit mode action is dispatched THEN update the mode and edit text states`() {
val store = BrowserToolbarStore()
val editMode = true
assertFalse(store.state.editMode)
assertEquals(Mode.DISPLAY, store.state.mode)
store.dispatch(BrowserToolbarAction.ToggleEditMode(editMode = editMode))
assertTrue(store.state.editMode)
assertEquals(Mode.EDIT, store.state.mode)
assertNull(store.state.editState.editText)
}

View File

@@ -96,7 +96,7 @@ fun BrowserScreen(navController: NavController) {
val loadUrl = components().sessionUseCases.loadUrl
BackHandler(enabled = toolbarState.editMode) {
BackHandler(enabled = toolbarState.isEditMode()) {
toolbarStore.dispatch(BrowserToolbarAction.ToggleEditMode(false))
}
@@ -126,7 +126,7 @@ fun BrowserScreen(navController: NavController) {
)
val url = toolbarState.editState.editText
if (toolbarState.editMode && url != null) {
if (toolbarState.isEditMode() && url != null) {
Suggestions(
url,
onSuggestionClicked = { suggestion ->

View File

@@ -32,6 +32,7 @@ enum class ToolbarConfiguration(val label: String) {
FENIX("Fenix"),
FENIX_CUSTOMTAB("Fenix (Custom Tab)"),
COMPOSE_TOOLBAR("Compose Toolbar"),
COMPOSE_CUSTOMTAB("Compose Custom Tab"),
}
class ConfigurationAdapter(

View File

@@ -41,6 +41,9 @@ import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.compose.browser.toolbar.BrowserToolbarDefaults
import mozilla.components.compose.browser.toolbar.CustomTabToolbarColors
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
import mozilla.components.compose.browser.toolbar.concept.Action.CustomAction
import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction
@@ -48,6 +51,7 @@ import mozilla.components.compose.browser.toolbar.store.BrowserToolbarState
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarStore
import mozilla.components.compose.browser.toolbar.store.DisplayState
import mozilla.components.compose.browser.toolbar.store.EditState
import mozilla.components.compose.browser.toolbar.store.Mode
import mozilla.components.compose.browser.toolbar.ui.SearchSelector
import mozilla.components.concept.menu.Side
import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
@@ -97,6 +101,7 @@ class ToolbarActivity : AppCompatActivity() {
ToolbarConfiguration.FENIX -> setupFenixToolbar()
ToolbarConfiguration.FENIX_CUSTOMTAB -> setupFenixCustomTabToolbar()
ToolbarConfiguration.COMPOSE_TOOLBAR -> setupComposeToolbar()
ToolbarConfiguration.COMPOSE_CUSTOMTAB -> setupComposeCustomTabToolbar()
}
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
@@ -531,7 +536,7 @@ class ToolbarActivity : AppCompatActivity() {
displayState = DisplayState(
hint = "Search or enter address",
pageActions = listOf(
Action.ActionButton(
ActionButton(
icon = iconsR.drawable.mozac_ic_arrow_clockwise_24,
contentDescription = null,
tint = iconPrimaryTint,
@@ -576,6 +581,58 @@ class ToolbarActivity : AppCompatActivity() {
}
}
private fun setupComposeCustomTabToolbar() {
showToolbar(isCompose = true)
binding.composeToolbar.setContent {
AcornTheme {
val iconPrimaryTint = AcornTheme.colors.iconPrimary.toArgb()
val store = remember {
BrowserToolbarStore(
initialState = BrowserToolbarState(
mode = Mode.CUSTOM_TAB,
displayState = DisplayState(
navigationActions = listOf(
ActionButton(
icon = iconsR.drawable.mozac_ic_cross_24,
contentDescription = null,
tint = iconPrimaryTint,
onClick = {},
),
),
browserActions = listOf(
ActionButton(
icon = iconsR.drawable.mozac_ic_arrow_clockwise_24,
contentDescription = null,
tint = iconPrimaryTint,
onClick = {},
),
),
),
),
)
}
BrowserToolbar(
store = store,
onDisplayToolbarClick = {},
onTextEdit = {},
onTextCommit = {},
colors = BrowserToolbarDefaults.colors(
customTabToolbarColors = CustomTabToolbarColors(
background = AcornTheme.colors.layer1,
title = AcornTheme.colors.textPrimary,
url = AcornTheme.colors.textSecondary,
),
),
url = "https://www.mozilla.org/en-US/firefox/mobile/",
title = "Mozilla",
)
}
}
}
private fun showToolbar(isCompose: Boolean = false) {
binding.toolbar.isVisible = !isCompose
binding.composeToolbar.isVisible = isCompose

View File

@@ -10,7 +10,9 @@ import mozilla.components.compose.browser.toolbar.BrowserDisplayToolbar
import mozilla.components.compose.browser.toolbar.BrowserEditToolbar
import mozilla.components.compose.browser.toolbar.BrowserToolbarColors
import mozilla.components.compose.browser.toolbar.BrowserToolbarDefaults
import mozilla.components.compose.browser.toolbar.CustomTabToolbar
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarStore
import mozilla.components.compose.browser.toolbar.store.Mode
import mozilla.components.lib.state.ext.observeAsState
/**
@@ -35,6 +37,7 @@ fun BrowserToolbar(
onTextCommit: (String) -> Unit,
colors: BrowserToolbarColors = BrowserToolbarDefaults.colors(),
url: String = "",
title: String = "",
) {
val uiState by store.observeAsState(initialValue = store.state) { it }
@@ -43,7 +46,7 @@ fun BrowserToolbar(
else -> editText
}
if (uiState.editMode) {
if (uiState.isEditMode()) {
BrowserEditToolbar(
url = input,
colors = colors.editToolbarColors,
@@ -64,4 +67,41 @@ fun BrowserToolbar(
},
)
}
when (uiState.mode) {
Mode.EDIT -> {
BrowserEditToolbar(
url = input,
colors = colors.editToolbarColors,
editActionsStart = uiState.editState.editActionsStart,
editActionsEnd = uiState.editState.editActionsEnd,
onUrlCommitted = { text -> onTextCommit(text) },
onUrlEdit = { text -> onTextEdit(text) },
)
}
Mode.DISPLAY -> {
BrowserDisplayToolbar(
url = url.takeIf { it.isNotEmpty() } ?: uiState.displayState.hint,
colors = colors.displayToolbarColors,
navigationActions = uiState.displayState.navigationActions,
pageActions = uiState.displayState.pageActions,
browserActions = uiState.displayState.browserActions,
onUrlClicked = {
onDisplayToolbarClick()
},
)
}
Mode.CUSTOM_TAB -> {
CustomTabToolbar(
url = url,
title = title,
colors = colors.customTabToolbarColor,
navigationActions = uiState.displayState.navigationActions,
pageActions = uiState.displayState.pageActions,
browserActions = uiState.displayState.browserActions,
)
}
}
}