[fenix] For https://github.com/mozilla-mobile/fenix/issues/21391 - Final design composables
Fonts are not exactly following the Figma design but do better suit the overall design since the other fonts are also not respecting the latest specs.pull/600/head
parent
710a94b9ed
commit
08a71a5366
@ -0,0 +1,97 @@
|
||||
/* 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.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = when (isSystemInDarkTheme()) {
|
||||
true -> PhotonColors.Violet40
|
||||
false -> PhotonColors.Violet70
|
||||
}
|
||||
),
|
||||
start = clickableStartIndex,
|
||||
end = clickableEndIndex
|
||||
)
|
||||
|
||||
addStyle(
|
||||
SpanStyle(textColor),
|
||||
start = clickableEndIndex,
|
||||
end = text.length
|
||||
)
|
||||
|
||||
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,84 @@
|
||||
/* 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.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.concept.fetch.MutableHeaders
|
||||
import mozilla.components.concept.fetch.Request
|
||||
import mozilla.components.concept.fetch.Response
|
||||
import mozilla.components.support.images.compose.loader.ImageLoader
|
||||
import mozilla.components.support.images.compose.loader.WithImage
|
||||
|
||||
/**
|
||||
* 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 client [Client] instance to be used for downloading the image.
|
||||
* When using [GeckoViewFetchClient] the image will automatically be cached if it has the right headers.
|
||||
* @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(
|
||||
client: Client,
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
private: Boolean = false,
|
||||
targetSize: Dp = 100.dp,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
ImageLoader(
|
||||
url = url,
|
||||
client = 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(
|
||||
FakeClient(),
|
||||
"https://mozilla.com",
|
||||
Modifier.height(100.dp).width(200.dp)
|
||||
)
|
||||
}
|
||||
|
||||
internal class FakeClient : Client() {
|
||||
override fun fetch(request: Request) = Response(
|
||||
url = request.url,
|
||||
status = 200,
|
||||
body = Response.Body.empty(),
|
||||
headers = MutableHeaders()
|
||||
)
|
||||
}
|
@ -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,160 @@
|
||||
/* 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 mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.concept.fetch.Client
|
||||
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 client [Client] instance to be used for downloading the image.
|
||||
* When using [GeckoViewFetchClient] the image will automatically be cached if it has the right headers.
|
||||
* @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(
|
||||
client: Client,
|
||||
imageUrl: String,
|
||||
title: String,
|
||||
caption: String? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
ListItemTabSurface(client, 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 client [Client] instance to be used for downloading the image.
|
||||
* When using [GeckoViewFetchClient] the image will automatically be cached if it has the right headers.
|
||||
* @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(
|
||||
client: Client,
|
||||
imageUrl: String,
|
||||
onClick: () -> Unit,
|
||||
title: @Composable () -> Unit,
|
||||
subtitle: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
ListItemTabSurface(client, imageUrl, onClick) {
|
||||
title()
|
||||
|
||||
subtitle?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared default configuration of a ListItemTabLarge Composable.
|
||||
*
|
||||
* @param client [Client] instance to be used for downloading the image.
|
||||
* When using [GeckoViewFetchClient] the image will automatically be cached if it has the right headers.
|
||||
* @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(
|
||||
client: Client,
|
||||
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(client, imageUrl, imageModifier, false, imageWidth)
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
tabDetails()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ListItemTabLargePreview() {
|
||||
FirefoxTheme {
|
||||
ListItemTabLarge(
|
||||
client = FakeClient(),
|
||||
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,48 @@
|
||||
/* 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
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun HeadingTextPreview() {
|
||||
SectionHeader(text = "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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="22dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:fillColor="#EF4056"
|
||||
android:pathData="M2.0993,0C0.7917,0.0852 -0,0.8317 -0,2.1595V10.1111C-0,16.5695 6.4234,21.6451 11.98,21.605C18.228,21.5599 24,16.2839 24,10.1111V2.1595C24,0.8267 23.1583,0.0751 21.8405,0H2.0993ZM7.1599,6.4685L11.98,11.048L16.805,6.4685C18.9645,5.5616 19.9065,8.0267 19.0196,8.7483L12.7365,14.7457C12.5311,14.9411 11.4338,14.9411 11.2284,14.7457L4.9453,8.7482C4.0985,7.9515 5.2008,5.4463 7.1599,6.4685Z" />
|
||||
</vector>
|
Loading…
Reference in New Issue