Merge tag 'v94.1.2' into upstream-sync
commit
a77b05cdc7
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
||||
package org.mozilla.fenix.helpers.assertions
|
||||
|
||||
import android.view.View
|
||||
import androidx.test.espresso.ViewAssertion
|
||||
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
|
||||
|
||||
class AwesomeBarAssertion {
|
||||
companion object {
|
||||
fun suggestionsAreGreaterThan(minimumSuggestions: Int): ViewAssertion {
|
||||
return ViewAssertion { view, noViewFoundException ->
|
||||
if (noViewFoundException != null) throw noViewFoundException
|
||||
|
||||
val suggestionsCount = getSuggestionCountFromView(view)
|
||||
|
||||
if (suggestionsCount <= minimumSuggestions)
|
||||
throw AssertionError("The suggestion count is less than or equal to the minimum suggestions")
|
||||
}
|
||||
}
|
||||
|
||||
fun suggestionsAreEqualTo(expectedItemCount: Int): ViewAssertion {
|
||||
return ViewAssertion { view, noViewFoundException ->
|
||||
if (noViewFoundException != null) throw noViewFoundException
|
||||
|
||||
val suggestionsCount = getSuggestionCountFromView(view)
|
||||
|
||||
if (suggestionsCount != expectedItemCount)
|
||||
throw AssertionError("The expected item count is $expectedItemCount, and the suggestions count within the AwesomeBar is $suggestionsCount")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSuggestionCountFromView(view: View): Int {
|
||||
return (view as BrowserAwesomeBar).adapter?.itemCount
|
||||
?: throw AssertionError("This view is not of type BrowserAwesomeBar")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.mozilla.fenix"
|
||||
android:sharedUserId="${sharedUserId}">
|
||||
<application
|
||||
android:name="org.mozilla.fenix.MigratingFenixApplication"
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity android:name=".autofill.AutofillUnlockActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
||||
|
||||
<activity android:name=".autofill.AutofillConfirmActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
||||
|
||||
<activity android:name=".autofill.AutofillSearchActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/DialogActivityTheme" />
|
||||
|
||||
<service
|
||||
android:name=".autofill.AutofillService"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.autofill.AutofillService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Overriding the alias of the main manifest to route app launches through our
|
||||
MigrationDecisionActivity which will show the migration screen before launching
|
||||
into the app if needed. -->
|
||||
<activity-alias
|
||||
android:name="${applicationId}.App"
|
||||
android:targetActivity="org.mozilla.fenix.MigrationDecisionActivity"
|
||||
tools:replace="android:targetActivity" />
|
||||
|
||||
<activity
|
||||
android:name="org.mozilla.fenix.MigrationDecisionActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name="org.mozilla.fenix.MigrationService" />
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,81 @@
|
||||
/* 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.android
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
|
||||
/**
|
||||
* Base [AppCompatDialogFragment] that adds behaviour to create a top or bottom dialog.
|
||||
*/
|
||||
abstract class FenixDialogFragment : AppCompatDialogFragment() {
|
||||
/**
|
||||
* Indicates the position of the dialog top or bottom.
|
||||
*/
|
||||
abstract val gravity: Int
|
||||
/**
|
||||
* The layout id that will be render on the dialog.
|
||||
*/
|
||||
abstract val layoutId: Int
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return if (gravity == Gravity.BOTTOM) {
|
||||
BottomSheetDialog(requireContext(), this.theme).apply {
|
||||
setOnShowListener {
|
||||
val bottomSheet =
|
||||
findViewById<View>(R.id.design_bottom_sheet) as FrameLayout
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Dialog(requireContext()).applyCustomizationsForTopDialog(inflateRootView())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Dialog.applyCustomizationsForTopDialog(rootView: View): Dialog {
|
||||
addContentView(
|
||||
rootView,
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
|
||||
window?.apply {
|
||||
setGravity(gravity)
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
// This must be called after addContentView, or it won't fully fill to the edge.
|
||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun inflateRootView(container: ViewGroup? = null): View {
|
||||
val contextThemeWrapper = ContextThemeWrapper(
|
||||
activity,
|
||||
(activity as HomeActivity).themeManager.currentThemeResource
|
||||
)
|
||||
return LayoutInflater.from(contextThemeWrapper).inflate(
|
||||
layoutId,
|
||||
container,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/* 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.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import mozilla.components.ui.colors.PhotonColors
|
||||
|
||||
/**
|
||||
* [Text] containing a substring styled as an URL informing when this is clicked.
|
||||
*
|
||||
* @param text Full text that will be displayed
|
||||
* @param textColor [Color] of the normal text. The URL substring will have a default URL style applied.
|
||||
* @param clickableStartIndex [text] index at which the URL substring starts.
|
||||
* @param clickableEndIndex [text] index at which the URL substring ends.
|
||||
* @param onClick Callback to be invoked only when the URL substring is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun ClickableSubstringLink(
|
||||
text: String,
|
||||
textColor: Color,
|
||||
clickableStartIndex: Int,
|
||||
clickableEndIndex: Int,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
append(text)
|
||||
|
||||
addStyle(
|
||||
SpanStyle(textColor),
|
||||
start = 0,
|
||||
end = clickableStartIndex
|
||||
)
|
||||
|
||||
addStyle(
|
||||
SpanStyle(
|
||||
color = when (isSystemInDarkTheme()) {
|
||||
true -> PhotonColors.Violet40
|
||||
false -> PhotonColors.Violet70
|
||||
}
|
||||
),
|
||||
start = clickableStartIndex,
|
||||
end = clickableEndIndex
|
||||
)
|
||||
|
||||
addStyle(
|
||||
SpanStyle(textColor),
|
||||
start = clickableEndIndex,
|
||||
end = text.length
|
||||
)
|
||||
|
||||
addStyle(
|
||||
SpanStyle(fontSize = 12.sp),
|
||||
start = 0,
|
||||
end = clickableEndIndex
|
||||
)
|
||||
|
||||
addStringAnnotation(
|
||||
tag = "link",
|
||||
annotation = "",
|
||||
start = clickableStartIndex,
|
||||
end = clickableEndIndex
|
||||
)
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedText,
|
||||
onClick = {
|
||||
annotatedText
|
||||
.getStringAnnotations("link", it, it)
|
||||
.firstOrNull()?.let {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ClickableSubstringTextPreview() {
|
||||
val text = "This text contains a link"
|
||||
Box(modifier = Modifier.background(PhotonColors.White)) {
|
||||
ClickableSubstringLink(
|
||||
text,
|
||||
PhotonColors.DarkGrey90,
|
||||
text.indexOf("link"),
|
||||
text.length
|
||||
) { }
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/* 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.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.support.images.compose.loader.ImageLoader
|
||||
import mozilla.components.support.images.compose.loader.WithImage
|
||||
import org.mozilla.fenix.components.components
|
||||
|
||||
/**
|
||||
* A composable that lays out and draws the image from a given URL while showing a default placeholder
|
||||
* while that image is downloaded or a default fallback image when downloading failed.
|
||||
*
|
||||
* @param url URL from where the to download the image to be shown.
|
||||
* @param modifier [Modifier] to be applied to the layout.
|
||||
* @param private Whether or not this is a private request. Like in private browsing mode,
|
||||
* private requests will not cache anything on disk and not send any cookies shared with the browser.
|
||||
* @param targetSize Image size (width and height) the loaded image should be scaled to.
|
||||
* @param contentDescription Localized text used by accessibility services to describe what this image represents.
|
||||
* This should always be provided unless this image is used for decorative purposes, and does not represent
|
||||
* a meaningful action that a user can take.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun Image(
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
private: Boolean = false,
|
||||
targetSize: Dp = 100.dp,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
ImageLoader(
|
||||
url = url,
|
||||
client = components.core.client,
|
||||
private = private,
|
||||
targetSize = targetSize
|
||||
) {
|
||||
WithImage { painter ->
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painter,
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
|
||||
WithDefaultPlaceholder(modifier, contentDescription)
|
||||
|
||||
WithDefaultFallback(modifier, contentDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ImagePreview() {
|
||||
Image(
|
||||
"https://mozilla.com",
|
||||
Modifier.height(100.dp).width(200.dp)
|
||||
)
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/* 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.Image
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.support.images.compose.loader.Fallback
|
||||
import mozilla.components.support.images.compose.loader.ImageLoaderScope
|
||||
import mozilla.components.support.images.compose.loader.Placeholder
|
||||
import mozilla.components.ui.colors.PhotonColors
|
||||
|
||||
/**
|
||||
* Renders the app default image placeholder while the image is still getting loaded.
|
||||
*
|
||||
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
|
||||
* @param contentDescription Text provided to accessibility services to describe what this image represents.
|
||||
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
|
||||
* accessibility services.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ImageLoaderScope.WithDefaultPlaceholder(
|
||||
modifier: Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
Placeholder {
|
||||
DefaultImagePlaceholder(modifier, contentDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the app default image placeholder if loading the image failed.
|
||||
*
|
||||
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
|
||||
* @param contentDescription Text provided to accessibility services to describe what this image represents.
|
||||
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
|
||||
* accessibility services.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ImageLoaderScope.WithDefaultFallback(
|
||||
modifier: Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
Fallback {
|
||||
DefaultImagePlaceholder(modifier, contentDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Application default image placeholder.
|
||||
*
|
||||
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
|
||||
* @param contentDescription Text provided to accessibility services to describe what this image represents.
|
||||
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
|
||||
* accessibility services.
|
||||
*/
|
||||
@Composable
|
||||
internal fun DefaultImagePlaceholder(
|
||||
modifier: Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
val color = when (isSystemInDarkTheme()) {
|
||||
true -> PhotonColors.DarkGrey30
|
||||
false -> PhotonColors.LightGrey30
|
||||
}
|
||||
|
||||
Image(ColorPainter(color), contentDescription, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun DefaultImagePlaceholderPreview() {
|
||||
DefaultImagePlaceholder(
|
||||
Modifier
|
||||
.size(200.dp, 100.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* 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.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* [FlingBehavior] for a [LazyRow] that will automatically scroll the list in the fling direction
|
||||
* to fully show the next item.
|
||||
*/
|
||||
@Composable
|
||||
fun EagerFlingBehavior(
|
||||
lazyRowState: LazyListState
|
||||
): FlingBehavior {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
return LazyListEagerFlingBehavior(lazyRowState, scope)
|
||||
}
|
||||
|
||||
private class LazyListEagerFlingBehavior(
|
||||
private val lazyRowState: LazyListState,
|
||||
private val scope: CoroutineScope
|
||||
) : FlingBehavior {
|
||||
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
|
||||
val firstItemIndex = lazyRowState.firstVisibleItemIndex
|
||||
|
||||
val itemIndexToScrollTo = when (initialVelocity <= 0) {
|
||||
true -> firstItemIndex
|
||||
false -> firstItemIndex + 1
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
lazyRowState.animateScrollToItem(itemIndexToScrollTo)
|
||||
}
|
||||
|
||||
return 0f // we've consumed the entire fling
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/* 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.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Default layout of a large tab shown in a list taking String arguments for title and caption.
|
||||
* Has the following structure:
|
||||
* ```
|
||||
* ---------------------------------------------
|
||||
* | -------------- Title |
|
||||
* | | Image | wrapped on |
|
||||
* | | from | three rows if needed |
|
||||
* | | imageUrl | |
|
||||
* | -------------- Optional caption |
|
||||
* ---------------------------------------------
|
||||
* ```
|
||||
*
|
||||
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
|
||||
* @param title Title off the tab this composable renders.
|
||||
* @param caption Optional caption text.
|
||||
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun ListItemTabLarge(
|
||||
imageUrl: String,
|
||||
title: String,
|
||||
caption: String? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
ListItemTabSurface(imageUrl, onClick) {
|
||||
TabTitle(text = title, maxLines = 3)
|
||||
|
||||
if (caption != null) {
|
||||
TabSubtitle(text = caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default layout of a large tab shown in a list taking composable arguments for title and caption
|
||||
* allowing as an exception to customize these elements.
|
||||
* Has the following structure:
|
||||
* ```
|
||||
* ---------------------------------------------
|
||||
* | -------------- -------------------------- |
|
||||
* | | | | Title | |
|
||||
* | | Image | | composable | |
|
||||
* | | from | -------------------------- |
|
||||
* | | imageUrl | -------------------------- |
|
||||
* | | | | Optional composable | |
|
||||
* | -------------- -------------------------- |
|
||||
* ---------------------------------------------
|
||||
* ```
|
||||
*
|
||||
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
|
||||
* @param title Composable rendering the title of the tab this composable represents.
|
||||
* @param subtitle Optional tab caption composable.
|
||||
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun ListItemTabLarge(
|
||||
imageUrl: String,
|
||||
onClick: () -> Unit,
|
||||
title: @Composable () -> Unit,
|
||||
subtitle: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
ListItemTabSurface(imageUrl, onClick) {
|
||||
title()
|
||||
|
||||
subtitle?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared default configuration of a ListItemTabLarge Composable.
|
||||
*
|
||||
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
|
||||
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||
* @param tabDetails [Composable] Displayed to the the end of the image. Allows for variation in the item text style.
|
||||
*/
|
||||
@Composable
|
||||
private fun ListItemTabSurface(
|
||||
imageUrl: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
tabDetails: @Composable () -> Unit
|
||||
) {
|
||||
var modifier = Modifier.size(328.dp, 116.dp)
|
||||
if (onClick != null) modifier = modifier.then(Modifier.clickable { onClick() })
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
backgroundColor = FirefoxTheme.colors.surface,
|
||||
elevation = 6.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
val (imageWidth, imageHeight) = 116.dp to 84.dp
|
||||
val imageModifier = Modifier
|
||||
.size(imageWidth, imageHeight)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
|
||||
Image(imageUrl, imageModifier, false, imageWidth)
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
tabDetails()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ListItemTabLargePreview() {
|
||||
FirefoxTheme {
|
||||
ListItemTabLarge(
|
||||
imageUrl = "",
|
||||
title = "This is a very long title for a tab but needs to be so for this preview",
|
||||
caption = "And this is a caption"
|
||||
) { }
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* 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.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Placeholder of a [ListItemTabLarge] with the same dimensions but only a centered text.
|
||||
* Has the following structure:
|
||||
* ```
|
||||
* ---------------------------------------------
|
||||
* | |
|
||||
* | |
|
||||
* | Placeholder text |
|
||||
* | |
|
||||
* | |
|
||||
* ---------------------------------------------
|
||||
* ```
|
||||
*
|
||||
* @param text The only [String] that this will display.
|
||||
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun ListItemTabLargePlaceholder(
|
||||
text: String,
|
||||
onClick: () -> Unit = { }
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(328.dp, 116.dp)
|
||||
.clickable { onClick() },
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
backgroundColor = FirefoxTheme.colors.surface,
|
||||
elevation = 6.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TextStyle(fontSize = 20.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ListItemTabLargePlaceholderPreview() {
|
||||
FirefoxTheme {
|
||||
ListItemTabLargePlaceholder(text = "Item placeholder")
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* 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.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Default layout for the header of a screen section.
|
||||
*
|
||||
* @param text [String] to be styled as header and displayed.
|
||||
* @param modifier [Modifier] to be applied to the [Text].
|
||||
*/
|
||||
@Composable
|
||||
fun SectionHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = FirefoxTheme.colors.textPrimary
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Default layout for the header of a screen section.
|
||||
*
|
||||
* @param text [String] to be styled as header and displayed.
|
||||
* @param modifier [Modifier] to be applied to the [Text].
|
||||
*/
|
||||
@Composable
|
||||
fun HomeSectionHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = FirefoxTheme.colors.textPrimary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun HeadingTextPreview() {
|
||||
SectionHeader(text = "Section title")
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun HomeHeadingTextPreview() {
|
||||
HomeSectionHeader(text = "Home section title")
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/* 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.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import mozilla.components.ui.colors.PhotonColors
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Default layout of a selectable chip.
|
||||
*
|
||||
* @param text [String] displayed in this chip. Ideally should only be one word.
|
||||
* @param isSelected Whether this should be shown as selected.
|
||||
* @param onClick Callback for when the user taps this.
|
||||
*/
|
||||
@Composable
|
||||
fun SelectableChip(
|
||||
text: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val contentColor = when (isSystemInDarkTheme()) {
|
||||
true -> PhotonColors.LightGrey10
|
||||
false -> if (isSelected) PhotonColors.LightGrey10 else PhotonColors.DarkGrey90
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val backgroundColor = when (isSystemInDarkTheme()) {
|
||||
true -> if (isSelected) PhotonColors.Violet50 else PhotonColors.DarkGrey50
|
||||
// Custom color codes matching the Figma design.
|
||||
false -> if (isSelected) { Color(0xFF312A65) } else { Color(0x1420123A) }
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.selectable(isSelected) { onClick() }
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(backgroundColor)
|
||||
.padding(16.dp, 10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text.capitalize(Locale.current),
|
||||
style = TextStyle(fontSize = 14.sp),
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun SelectableChipPreview() {
|
||||
FirefoxTheme {
|
||||
Box(Modifier.fillMaxSize().background(FirefoxTheme.colors.surface)) {
|
||||
SelectableChip("Chirp", false) { }
|
||||
SelectableChip(text = "Chirp", isSelected = true) { }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/* 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.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
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 a list of items as a staggered horizontal grid placing them on ltr rows and continuing
|
||||
* on as many below rows as needed to place all items.
|
||||
*
|
||||
* In an effort to best utilize the available row space this can mix the items such that narrower ones
|
||||
* are placed on the same row as wider ones if the otherwise next item doesn't fit.
|
||||
*
|
||||
* @param modifier [Modifier] to be applied to the layout.
|
||||
* @param horizontalItemsSpacing Minimum horizontal space between items. Does not add spacing to layout bounds.
|
||||
* @param verticalItemsSpacing Vertical space between items
|
||||
* @param arrangement How the items will be horizontally aligned and spaced.
|
||||
* @param content The children composables to be laid out.
|
||||
*/
|
||||
@Composable
|
||||
fun StaggeredHorizontalGrid(
|
||||
modifier: Modifier = Modifier,
|
||||
horizontalItemsSpacing: Dp = 0.dp,
|
||||
verticalItemsSpacing: Dp = 8.dp,
|
||||
arrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val currentLayoutDirection = LocalLayoutDirection.current
|
||||
|
||||
Layout(content, modifier) { items, constraints ->
|
||||
val horizontalItemsSpacingPixels = horizontalItemsSpacing.roundToPx()
|
||||
val verticalItemsSpacingPixels = verticalItemsSpacing.roundToPx()
|
||||
var totalHeight = 0
|
||||
val itemsRows = mutableListOf<List<Placeable>>()
|
||||
val notYetPlacedItems = items.map {
|
||||
it.measure(constraints)
|
||||
}.toMutableList()
|
||||
|
||||
fun getIndexOfNextPlaceableThatFitsRow(available: List<Placeable>, currentWidth: Int): Int {
|
||||
return available.indexOfFirst {
|
||||
currentWidth + it.width <= constraints.maxWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Populate each row with as many items as possible combining wider with narrower items.
|
||||
// This will change the order of shown categories.
|
||||
var (currentRow, currentWidth) = mutableListOf<Placeable>() to 0
|
||||
while (notYetPlacedItems.isNotEmpty()) {
|
||||
if (currentRow.isEmpty()) {
|
||||
currentRow.add(
|
||||
notYetPlacedItems[0].also {
|
||||
currentWidth += it.width + horizontalItemsSpacingPixels
|
||||
totalHeight += it.height + verticalItemsSpacingPixels
|
||||
}
|
||||
)
|
||||
notYetPlacedItems.removeAt(0)
|
||||
} else {
|
||||
val nextPlaceableThatFitsIndex = getIndexOfNextPlaceableThatFitsRow(notYetPlacedItems, currentWidth)
|
||||
if (nextPlaceableThatFitsIndex >= 0) {
|
||||
currentRow.add(
|
||||
notYetPlacedItems[nextPlaceableThatFitsIndex].also {
|
||||
currentWidth += it.width + horizontalItemsSpacingPixels
|
||||
}
|
||||
)
|
||||
notYetPlacedItems.removeAt(nextPlaceableThatFitsIndex)
|
||||
} else {
|
||||
itemsRows.add(currentRow)
|
||||
currentRow = mutableListOf()
|
||||
currentWidth = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentRow.isNotEmpty()) {
|
||||
itemsRows.add(currentRow)
|
||||
}
|
||||
totalHeight -= verticalItemsSpacingPixels
|
||||
|
||||
// Place each item from each row on screen.
|
||||
layout(constraints.maxWidth, totalHeight) {
|
||||
itemsRows.forEachIndexed { rowIndex, itemRow ->
|
||||
val itemsSizes = IntArray(itemRow.size) {
|
||||
itemRow[it].width + when (currentLayoutDirection == LayoutDirection.Ltr) {
|
||||
true -> if (it < itemRow.lastIndex) horizontalItemsSpacingPixels else 0
|
||||
false -> if (it > 0) horizontalItemsSpacingPixels else 0
|
||||
}
|
||||
}
|
||||
val itemsPositions = IntArray(itemsSizes.size) { 0 }
|
||||
with(arrangement) {
|
||||
arrange(constraints.maxWidth, itemsSizes, currentLayoutDirection, itemsPositions)
|
||||
}
|
||||
|
||||
itemRow.forEachIndexed { itemIndex, item ->
|
||||
item.place(
|
||||
x = itemsPositions[itemIndex],
|
||||
y = (rowIndex * item.height) + (rowIndex * verticalItemsSpacingPixels)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun StaggeredHorizontalGridPreview() {
|
||||
FirefoxTheme {
|
||||
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||
StaggeredHorizontalGrid(
|
||||
horizontalItemsSpacing = 8.dp,
|
||||
arrangement = Arrangement.Center
|
||||
) {
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
|
||||
.split(" ")
|
||||
.forEach {
|
||||
Text(text = it, color = Color.Red, modifier = Modifier.border(3.dp, Color.Blue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/* 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.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Default layout for a tab composable caption.
|
||||
*
|
||||
* @param text Tab caption.
|
||||
* @param modifier Optional [Modifier] to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
fun TabSubtitle(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
maxLines = 1,
|
||||
text = text,
|
||||
style = TextStyle(fontSize = 12.sp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = FirefoxTheme.colors.textSecondary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun TabSubtitlePreview() {
|
||||
FirefoxTheme {
|
||||
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||
TabSubtitle(
|
||||
"Awesome tab subtitle",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/* 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.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Special caption text for a tab layout shown on one line.
|
||||
*
|
||||
* This will combine [firstText] with a interdot and then [secondText] ensuring that the second text
|
||||
* (which is assumed to be smaller) always fills as much space as needed with the [firstText] automatically
|
||||
* being resized to be smaller with an added ellipsis characters if needed.
|
||||
*
|
||||
* Possible results:
|
||||
* ```
|
||||
* - when both texts would fit the screen
|
||||
* ------------------------------------------
|
||||
* |firstText · secondText |
|
||||
* ------------------------------------------
|
||||
*
|
||||
* - when both text do not fit, second is shown in entirety, first is ellipsised.
|
||||
* ------------------------------------------
|
||||
* |longerFirstTextOrSmallSc... · secondText|
|
||||
* ------------------------------------------
|
||||
* ```
|
||||
*
|
||||
* @param firstText Text shown at the start of the row.
|
||||
* @param secondText Text shown at the end of the row.
|
||||
*/
|
||||
@Composable
|
||||
fun TabSubtitleWithInterdot(
|
||||
firstText: String,
|
||||
secondText: String,
|
||||
) {
|
||||
val currentLayoutDirection = LocalLayoutDirection.current
|
||||
|
||||
Layout(
|
||||
content = {
|
||||
TabSubtitle(text = firstText)
|
||||
TabSubtitle(text = " \u00b7 ")
|
||||
TabSubtitle(text = secondText)
|
||||
}
|
||||
) { items, constraints ->
|
||||
|
||||
// We need to measure from the end to start to ensure the secondItem will always be on screen
|
||||
// and depending on secondItem's width and interdot's width the firstItem is automatically resized.
|
||||
val secondItem = items[2].measure(constraints)
|
||||
val interdot = items[1].measure(
|
||||
constraints.copy(maxWidth = constraints.maxWidth - secondItem.width)
|
||||
)
|
||||
val firstItem = items[0].measure(
|
||||
constraints.copy(maxWidth = constraints.maxWidth - secondItem.width - interdot.width)
|
||||
)
|
||||
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
val itemsPositions = IntArray(items.size)
|
||||
with(Arrangement.Start) {
|
||||
arrange(
|
||||
constraints.maxWidth,
|
||||
intArrayOf(firstItem.width, interdot.width, secondItem.width),
|
||||
currentLayoutDirection,
|
||||
itemsPositions
|
||||
)
|
||||
}
|
||||
|
||||
val placementHeight = constraints.maxHeight - firstItem.height
|
||||
listOf(firstItem, interdot, secondItem).forEachIndexed { index, item ->
|
||||
item.place(itemsPositions[index], placementHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun TabSubtitleWithInterdotPreview() {
|
||||
FirefoxTheme {
|
||||
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||
TabSubtitleWithInterdot(
|
||||
firstText = "firstText",
|
||||
secondText = "secondText",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/* 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.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Default layout for a tab composable title.
|
||||
*
|
||||
* @param text Tab title
|
||||
* @param maxLines Maximum number of lines for [text] to span, wrapping if necessary.
|
||||
* If the text exceeds the given number of lines it will be ellipsized.
|
||||
* @param modifier Optional [Modifier] to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
fun TabTitle(
|
||||
text: String,
|
||||
maxLines: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
maxLines = maxLines,
|
||||
text = text,
|
||||
style = TextStyle(fontSize = 14.sp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = FirefoxTheme.colors.textPrimary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabTitlePreview() {
|
||||
FirefoxTheme {
|
||||
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||
TabTitle(
|
||||
"Awesome tab title",
|
||||
2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue