summaryrefslogtreecommitdiffhomepage
path: root/android/lib/ui/designsystem
diff options
context:
space:
mode:
Diffstat (limited to 'android/lib/ui/designsystem')
-rw-r--r--android/lib/ui/designsystem/build.gradle.kts43
-rw-r--r--android/lib/ui/designsystem/src/main/AndroidManifest.xml2
-rw-r--r--android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt56
-rw-r--r--android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt78
-rw-r--r--android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt313
5 files changed, 492 insertions, 0 deletions
diff --git a/android/lib/ui/designsystem/build.gradle.kts b/android/lib/ui/designsystem/build.gradle.kts
new file mode 100644
index 0000000000..efc9d0108b
--- /dev/null
+++ b/android/lib/ui/designsystem/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.compose)
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.lib.ui.designsystem"
+ compileSdk = libs.versions.compile.sdk.get().toInt()
+ buildToolsVersion = libs.versions.build.tools.get()
+
+ defaultConfig { minSdk = libs.versions.min.sdk.get().toInt() }
+
+ buildFeatures { compose = true }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.jvm.target.get()
+ allWarningsAsErrors = true
+ }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+}
+
+dependencies {
+ implementation(projects.lib.theme)
+ implementation(projects.lib.model)
+ implementation(projects.lib.ui.tag)
+
+ implementation(libs.compose.ui)
+ implementation(libs.compose.ui.tooling)
+ implementation(libs.compose.ui.tooling.preview)
+ implementation(libs.compose.material3)
+ implementation(libs.compose.icons.extended)
+}
diff --git a/android/lib/ui/designsystem/src/main/AndroidManifest.xml b/android/lib/ui/designsystem/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..b2d3ea1235
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt
new file mode 100644
index 0000000000..15e9556e47
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/Checkbox.kt
@@ -0,0 +1,56 @@
+package net.mullvad.mullvadvpn.lib.ui.designsystem
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Checkbox as Material3Checkbox
+import androidx.compose.material3.CheckboxColors
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.selected
+
+@Composable
+fun Checkbox(
+ checked: Boolean,
+ onCheckedChange: ((Boolean) -> Unit)?,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: CheckboxColors =
+ CheckboxDefaults.colors(
+ checkedColor = MaterialTheme.colorScheme.onPrimary,
+ uncheckedColor = MaterialTheme.colorScheme.onPrimary,
+ checkmarkColor = MaterialTheme.colorScheme.selected,
+ ),
+ interactionSource: MutableInteractionSource? = null,
+) {
+ Material3Checkbox(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource,
+ )
+}
+
+@Preview
+@Composable
+private fun PreviewCheckbox() {
+ AppTheme {
+ Column(
+ Modifier.background(color = MaterialTheme.colorScheme.background),
+ verticalArrangement = Arrangement.spacedBy(Dimens.smallSpacer),
+ ) {
+ Checkbox(checked = false, null)
+ Checkbox(checked = true, null)
+ Checkbox(checked = false, null, enabled = false)
+ Checkbox(checked = true, null, enabled = false)
+ }
+ }
+}
diff --git a/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt
new file mode 100644
index 0000000000..3dc20ff0e7
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListHeader.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.lib.ui.designsystem
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+
+private val LIST_HEADER_MIN_HEIGHT = 48.dp
+
+@Composable
+fun RelayListHeader(
+ content: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ actions: @Composable (RowScope.() -> Unit)? = null,
+) {
+ ProvideContentColorTextStyle(
+ MaterialTheme.colorScheme.onBackground,
+ MaterialTheme.typography.bodyLarge,
+ ) {
+ Row(
+ modifier =
+ Modifier.padding(horizontal = Dimens.tinyPadding)
+ .defaultMinSize(minHeight = LIST_HEADER_MIN_HEIGHT)
+ .height(IntrinsicSize.Min)
+ .then(modifier),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ content()
+ HorizontalDivider(
+ Modifier.weight(1f, true).padding(start = Dimens.smallPadding),
+ color =
+ MaterialTheme.colorScheme.onBackground.copy(
+ alpha = RelayListHeaderTokens.RelayListHeaderDividerAlpha
+ ),
+ )
+ actions?.invoke(this)
+ }
+ }
+}
+
+object RelayListHeaderTokens {
+ const val RelayListHeaderDividerAlpha = 0.2f
+}
+
+@Preview(backgroundColor = 0xFF192E45, showBackground = true)
+@Composable
+fun PreviewRelayListHeader() {
+ AppTheme {
+ Column {
+ RelayListHeader(content = { Text("Header") })
+ RelayListHeader(
+ content = { Text("Header") },
+ actions = {
+ IconButton(onClick = {}) {
+ Icon(imageVector = Icons.Default.Edit, contentDescription = null)
+ }
+ },
+ )
+ }
+ }
+}
diff --git a/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt
new file mode 100644
index 0000000000..c2e9664a18
--- /dev/null
+++ b/android/lib/ui/designsystem/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/designsystem/RelayListItem.kt
@@ -0,0 +1,313 @@
+package net.mullvad.mullvadvpn.lib.ui.designsystem
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewFontScale
+import androidx.compose.ui.unit.dp
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
+
+@Composable
+fun RelayListItem(
+ modifier: Modifier = Modifier,
+ selected: Boolean = false,
+ enabled: Boolean = true,
+ onClick: (() -> Unit) = {},
+ onLongClick: (() -> Unit)? = {},
+ leadingContent: @Composable (() -> Unit)? = null,
+ content: @Composable () -> Unit,
+ trailingContent: @Composable (() -> Unit)? = null,
+ colors: RelayListItemColors = RelayListItemDefaults.colors(),
+ shape: Shape = RectangleShape,
+) {
+ Surface(
+ modifier =
+ modifier
+ .defaultMinSize(minHeight = RelayListTokens.listItemMinHeight)
+ .height(IntrinsicSize.Min),
+ shape = shape,
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(RelayListTokens.listItemSpacer),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (leadingContent != null) {
+ Box(
+ Modifier.background(colors.containerColor)
+ .width(RelayListTokens.listItemButtonWidth)
+ .fillMaxHeight(),
+ contentAlignment = Alignment.Center,
+ ) {
+ ProvideContentColorTextStyle(
+ colors.leadingIconColor,
+ MaterialTheme.typography.titleMedium,
+ ) {
+ leadingContent()
+ }
+ }
+ }
+
+ Row(
+ Modifier.weight(1f, fill = true)
+ .background(colors.containerColor)
+ .fillMaxHeight()
+ .combinedClickable(
+ enabled = true,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ ProvideContentColorTextStyle(
+ colors.headlineColor(enabled, selected),
+ MaterialTheme.typography.titleMedium,
+ ) {
+ content()
+ }
+ }
+
+ if (trailingContent != null) {
+ Box(
+ Modifier.background(color = colors.containerColor)
+ .width(RelayListTokens.listItemButtonWidth)
+ .fillMaxHeight()
+ ) {
+ ProvideContentColorTextStyle(
+ colors.trailingIconColor,
+ MaterialTheme.typography.titleMedium,
+ ) {
+ trailingContent()
+ }
+ }
+ }
+ }
+ }
+}
+
+// Based of ListItem
+@Immutable
+class RelayListItemColors(
+ val containerColor: Color,
+ val headlineColor: Color,
+ val leadingIconColor: Color,
+ val trailingIconColor: Color,
+ val selectedHeadlineColor: Color,
+ val disabledHeadlineColor: Color,
+) {
+ internal fun containerColor(): Color = containerColor
+
+ @Stable
+ internal fun headlineColor(enabled: Boolean, selected: Boolean): Color =
+ when {
+ !enabled -> disabledHeadlineColor
+ selected -> selectedHeadlineColor
+ else -> headlineColor
+ }
+}
+
+@Composable
+internal fun ProvideContentColorTextStyle(
+ contentColor: Color,
+ textStyle: TextStyle,
+ content: @Composable () -> Unit,
+) {
+ val mergedStyle = LocalTextStyle.current.merge(textStyle)
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ LocalTextStyle provides mergedStyle,
+ content = content,
+ )
+}
+
+object RelayListItemDefaults {
+ @Composable
+ fun colors(
+ containerColor: Color = MaterialTheme.colorScheme.surface,
+ headlineColor: Color = MaterialTheme.colorScheme.onSurface,
+ leadingIconColor: Color = MaterialTheme.colorScheme.onSurface,
+ trailingIconColor: Color = MaterialTheme.colorScheme.onSurface,
+ selectedHeadlineColor: Color = MaterialTheme.colorScheme.tertiary,
+ disabledHeadlineColor: Color =
+ headlineColor.copy(alpha = RelayListTokens.RelayListItemDisabledLabelTextOpacity),
+ ): RelayListItemColors =
+ RelayListItemColors(
+ containerColor = containerColor,
+ headlineColor = headlineColor,
+ leadingIconColor = leadingIconColor,
+ trailingIconColor = trailingIconColor,
+ selectedHeadlineColor = selectedHeadlineColor,
+ disabledHeadlineColor = disabledHeadlineColor,
+ )
+}
+
+object RelayListTokens {
+ const val RelayListItemDisabledLabelTextOpacity = AlphaInactive
+
+ val listItemMinHeight = 56.dp
+ val listItemSpacer = 2.dp
+ val listItemButtonWidth = 56.dp
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewSimpleRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ content = { Text("Hello world", modifier = Modifier.padding(16.dp).fillMaxSize()) },
+ )
+ }
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewLeadingRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ content = {
+ Text(
+ "Hello world fsadhkuhfiuskahf iuhsadhuf sa",
+ modifier =
+ Modifier.padding(16.dp)
+ .fillMaxSize()
+ .wrapContentHeight(align = Alignment.CenterVertically),
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ leadingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = { /* Handle click */ }),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewTrailingRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ selected = true,
+ content = {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(imageVector = Icons.Default.Check, contentDescription = null)
+ Spacer(Modifier.width(8.dp))
+ Text(
+ "Hello world fsadhkuhfiuskahf iuhsadhuf sa",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ },
+ trailingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.Add,
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+}
+
+@Preview
+@PreviewFontScale
+@Composable
+private fun PreviewLeadingAndTrailingRelayListItem() {
+ AppTheme {
+ RelayListItem(
+ modifier = Modifier.fillMaxWidth(),
+ content = {
+ Text(
+ "Hello world iuhsadhuf sa",
+ modifier = Modifier.clickable {}.padding(16.dp).fillMaxSize(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ leadingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ )
+ }
+ },
+ trailingContent = {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.padding(16.dp),
+ imageVector = Icons.Default.Add,
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+}