diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
index dc499c7f5..8132b30ec 100644
--- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt
@@ -551,6 +551,7 @@ class SettingsRepository(
const val KEY_ISLAND_BATTERY_HIDE_LOCKED = "island_battery_hide_locked"
const val KEY_ISLAND_TIMEOUT_MS = "island_timeout_ms"
const val KEY_ISLAND_SHOW_GLOW = "island_show_glow"
+ const val KEY_ISLAND_NOTIF_THEMED_BUBBLE = "island_notif_themed_bubble"
const val KEY_ISLAND_EXPANDED_WIDTH = "island_expanded_width"
const val KEY_ISLAND_EXPANDED_ROUNDNESS = "island_expanded_roundness"
const val KEY_ISLAND_EXPANDED_PADDING = "island_expanded_padding"
@@ -4106,6 +4107,12 @@ class SettingsRepository(
fun isIslandShowGlowEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_GLOW, true)
fun setIslandShowGlowEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_GLOW, enabled)
+ fun isIslandNotifThemedBubbleEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_THEMED_BUBBLE, false)
+ fun setIslandNotifThemedBubbleEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_THEMED_BUBBLE, enabled)
+
+ fun isIslandNotifThemedBubbleActive(): Boolean =
+ isIslandNotifThemedBubbleEnabled() && getBoolean(KEY_ISLAND_THEMED_APP_ICONS, false)
+
fun getIslandExpandedWidth(): Float = getFloat(KEY_ISLAND_EXPANDED_WIDTH, 360f)
fun setIslandExpandedWidth(value: Float) = putFloat(KEY_ISLAND_EXPANDED_WIDTH, value)
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt
index 1ec301ff1..d16a336aa 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt
@@ -32,5 +32,7 @@ data class ActiveNotificationAlert(
val ongoing: Boolean = false,
val isConversation: Boolean = false,
val hubOnly: Boolean = false,
+ val themedIcon: Bitmap? = null,
+ val smallIcon: Bitmap? = null,
)
diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt
index 49fcfa500..afe29dc2f 100644
--- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt
+++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt
@@ -887,6 +887,11 @@ object FeatureRegistry {
R.string.island_notification_options_title,
"island_notif_hub",
),
+ SearchSetting(
+ R.string.island_notif_themed_bubble_title,
+ R.string.island_notif_hub_title,
+ "island_notif_hub",
+ ),
SearchSetting(
R.string.island_show_calls_title,
R.string.island_call_incoming,
diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt
index 12fc9de6e..ffb0ef1f0 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt
@@ -11,6 +11,9 @@ import android.app.KeyguardManager
import android.content.Context
import android.os.PowerManager
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.runtime.Composable
+import android.graphics.Bitmap
import androidx.compose.ui.unit.dp
import com.sameerasw.essentials.R
import com.sameerasw.essentials.data.repository.SettingsRepository
@@ -34,6 +37,7 @@ import com.sameerasw.essentials.island.plugins.soften
import com.sameerasw.essentials.island.ui.IslandStackBubble
import com.sameerasw.essentials.island.ui.IslandStackOverlap
import com.sameerasw.essentials.island.ui.components.IslandBitmap
+import com.sameerasw.essentials.island.ui.components.NotificationGlyph
import com.sameerasw.essentials.island.ui.components.IslandIcon
import com.sameerasw.essentials.services.NotificationListener
import com.sameerasw.essentials.utils.HapticUtil
@@ -48,6 +52,8 @@ class NotificationsPlugin : BaseIslandPlugin() {
SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE,
SettingsRepository.KEY_ISLAND_LINE_STAGE_ENABLED,
SettingsRepository.KEY_ISLAND_SHOW_GLOW,
+ SettingsRepository.KEY_ISLAND_NOTIF_THEMED_BUBBLE,
+ SettingsRepository.KEY_ISLAND_THEMED_APP_ICONS,
SettingsRepository.KEY_ISLAND_NOTIF_QUEUE,
SettingsRepository.KEY_ISLAND_NOTIF_SKIP_SILENT,
SettingsRepository.KEY_ISLAND_NOTIF_FOLLOW_DND,
@@ -565,7 +571,7 @@ class NotificationsPlugin : BaseIslandPlugin() {
if (!entry.grouped) return stackIconFor(entry.lead, current = false, highlight = highlight)
val icon = entry.lead.appIcon ?: entry.lead.icon
return StackIcon(entry.key, onSelect = { select(entry.lead.key) }, highlight = highlight) { size ->
- IslandBitmap(icon, size, circle = true, fallbackRes = R.drawable.rounded_notifications_unread_24)
+ AlertIcon(entry.lead, icon, size, circle = true)
}
}
@@ -745,16 +751,16 @@ class NotificationsPlugin : BaseIslandPlugin() {
compact = buildList {
add(
CompactCell("notif.icon") {
- IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24)
+ AlertIcon(alert, icon, 22.dp)
},
)
val appIcon = alert.appIcon
- if (!minimal() && alert.chatIcon != null && appIcon != null) {
+ if (!minimal() && !themedBubble() && alert.chatIcon != null && appIcon != null) {
add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) })
}
},
line = LineContent(
- icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) },
+ icon = { AlertIcon(alert, icon, 24.dp) },
start = sender,
end = message,
condensed = condensedLine(),
@@ -802,24 +808,24 @@ class NotificationsPlugin : BaseIslandPlugin() {
compact = buildList {
add(
CompactCell("notif.icon") {
- IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24)
+ AlertIcon(alert, icon, 22.dp)
},
)
val appIcon = alert.appIcon
- if (!minimal() && !hideChat && alert.chatIcon != null && appIcon != null) {
+ if (!minimal() && !themedBubble() && !hideChat && alert.chatIcon != null && appIcon != null) {
add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) })
}
},
line = if (isOtp(alert)) {
LineContent(
- icon = { IslandBitmap(alert.appIcon ?: alert.icon, 24.dp, fallbackRes = R.drawable.rounded_lock_24) },
+ icon = { AlertIcon(alert, alert.appIcon ?: alert.icon, 24.dp, fallbackRes = R.drawable.rounded_lock_24) },
start = "${appNameFor(context, alert)} ยท ${context.getString(R.string.island_otp_label)}",
end = alert.otpCode?.let(::maskOtp).orEmpty(),
endSlot = { IslandIcon(R.drawable.rounded_lock_24, size = 18.dp) },
)
} else {
LineContent(
- icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) },
+ icon = { AlertIcon(alert, icon, 24.dp) },
start = appNameFor(context, alert),
end = "",
condensed = condensedLine(),
@@ -853,11 +859,11 @@ class NotificationsPlugin : BaseIslandPlugin() {
placement = CompactPlacement.Dynamic,
compact = listOf(
CompactCell("notif.icon") {
- IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24)
+ AlertIcon(alert, icon, 22.dp)
},
),
line = LineContent(
- icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) },
+ icon = { AlertIcon(alert, icon, 24.dp) },
start = sender,
end = formatOtp(code),
endSlot = { IslandIcon(R.drawable.rounded_content_copy_24, size = 18.dp) },
@@ -943,7 +949,24 @@ class NotificationsPlugin : BaseIslandPlugin() {
private fun stackIconFor(alert: ActiveNotificationAlert, current: Boolean, highlight: Boolean = false): StackIcon {
val icon = if ((concealed() && hidesChatPictures()) || (hub() && !settings.isIslandNotifHubPreferChatPictures())) alert.appIcon ?: alert.icon else alert.chatIcon ?: alert.appIcon ?: alert.icon
return StackIcon(alert.key, current = current, onSelect = { select(alert.key) }, highlight = highlight) { size ->
- IslandBitmap(icon, size, circle = true, fallbackRes = R.drawable.rounded_notifications_unread_24)
+ AlertIcon(alert, icon, size, circle = true)
+ }
+ }
+
+ private fun themedBubble() = settings.isIslandNotifThemedBubbleActive()
+
+ @Composable
+ private fun AlertIcon(
+ alert: ActiveNotificationAlert,
+ bitmap: Bitmap?,
+ size: Dp,
+ circle: Boolean = false,
+ fallbackRes: Int = R.drawable.rounded_notifications_unread_24,
+ ) {
+ if (themedBubble()) {
+ NotificationGlyph(alert.themedIcon, alert.smallIcon, alert.appColor?.let { Color(soften(it)) }, size, fallbackRes = fallbackRes)
+ } else {
+ IslandBitmap(bitmap, size, circle = circle, fallbackRes = fallbackRes)
}
}
diff --git a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt
index b550b1e41..028e1db92 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt
@@ -731,7 +731,6 @@ class IslandCoordinator(
controller.balanceCompact = settings.isIslandBalanceCompactEnabled()
controller.chipsEnabled = settings.getBoolean(SettingsRepository.KEY_ISLAND_CHIPS, false)
controller.secondaryBubbleEnabled = settings.isIslandSecondaryBubbleEnabled()
- controller.secondaryBubbleOverlap = settings.getIslandNotifQueueStyle() == SettingsRepository.ISLAND_NOTIF_QUEUE_STYLE_OVERLAP
controller.relayout()
controller.expandedTimeoutMs = settings.getIslandExpandedTimeoutMs()
applyPreviewRing()
@@ -825,6 +824,7 @@ class IslandCoordinator(
"alarm" to SettingsRepository.KEY_ISLAND_ALARM_LAUNCHER_ONLY,
)
val CONFIG_KEYS = setOf(
+ SettingsRepository.KEY_ISLAND_NOTIF_THEMED_BUBBLE,
SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_STYLE,
SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE,
SettingsRepository.KEY_ISLAND_USE_AUTO_DETECT,
diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt
index c9df3f1a6..2b1df6627 100644
--- a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt
+++ b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt
@@ -53,15 +53,51 @@ import com.sameerasw.essentials.island.ui.IslandTextStyles
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.drawBehind
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.Row
import com.sameerasw.essentials.island.ui.IslandMotion
+import androidx.compose.foundation.background
import kotlinx.coroutines.delay
import kotlin.random.Random
+@Composable
+fun NotificationGlyph(
+ themedIcon: Bitmap?,
+ smallIcon: Bitmap?,
+ color: Color?,
+ size: Dp,
+ modifier: Modifier = Modifier,
+ fallbackRes: Int,
+) {
+ val background = color ?: MaterialTheme.colorScheme.primary
+ val glyph = lerp(background, if (background.luminance() > 0.179f) Color.Black else Color.White, 0.8f)
+ Box(
+ modifier
+ .sizeIn(maxWidth = size, maxHeight = size)
+ .aspectRatio(1f, matchHeightConstraintsFirst = true)
+ .clip(CircleShape)
+ .background(background),
+ contentAlignment = Alignment.Center,
+ ) {
+ val icon = themedIcon ?: smallIcon
+ if (icon != null) {
+ Image(
+ bitmap = remember(icon) { icon.asImageBitmap() },
+ contentDescription = null,
+ contentScale = ContentScale.Fit,
+ colorFilter = ColorFilter.tint(glyph),
+ modifier = Modifier.fillMaxSize(if (themedIcon != null) 1f else 0.6f),
+ )
+ } else {
+ Icon(painterResource(fallbackRes), null, tint = glyph, modifier = Modifier.fillMaxSize(0.6f))
+ }
+ }
+}
+
@Composable
fun IslandBitmap(
bitmap: Bitmap?,
diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt
index 929b0a581..2fb45a60b 100644
--- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt
+++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt
@@ -2353,12 +2353,23 @@ class NotificationListener : NotificationListenerService() {
bitmap = appIcon
}
+ var smallIconBitmap: Bitmap? = null
+ try {
+ notif.smallIcon?.loadDrawable(this)?.let { smallIconBitmap = AppUtil.drawableToBitmap(it) }
+ } catch (_: Exception) {}
+
+ val themedBubble = SettingsRepository(this).isIslandNotifThemedBubbleActive()
+ val paletteSource = if (themedBubble) {
+ runCatching { AppUtil.drawableToBitmap(packageManager.getApplicationIcon(sbn.packageName)) }.getOrNull() ?: bitmap
+ } else {
+ bitmap
+ }
var appColor: Int? = null
if (notif.color != 0 && notif.color != android.graphics.Color.TRANSPARENT) {
appColor = notif.color
- } else if (bitmap != null) {
+ } else if (paletteSource != null) {
try {
- val palette = androidx.palette.graphics.Palette.from(bitmap).generate()
+ val palette = androidx.palette.graphics.Palette.from(paletteSource).generate()
val vibrant = palette.getVibrantColor(0)
val dominant = palette.getDominantColor(0)
if (vibrant != 0) {
@@ -2403,6 +2414,8 @@ class NotificationListener : NotificationListenerService() {
appIcon = appIcon,
actions = actionList,
chatIcon = chatIcon,
+ themedIcon = if (themedBubble) AppIconUtil.monochromeAppIcon(this, sbn.packageName) else null,
+ smallIcon = smallIconBitmap,
)
}
diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt
index 46ae00f8d..a7e2c63ca 100644
--- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt
+++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt
@@ -8,6 +8,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
@@ -29,6 +31,8 @@ fun IslandNotificationHubOptionsBottomSheet(
onDismissRequest: () -> Unit,
) {
val view = LocalView.current
+ val context = LocalContext.current
+ val themedAppIcons = remember { SettingsRepository(context).getBoolean(SettingsRepository.KEY_ISLAND_THEMED_APP_ICONS, false) }
EssentialsBottomSheet(onDismissRequest = onDismissRequest) {
Column(
@@ -90,11 +94,23 @@ fun IslandNotificationHubOptionsBottomSheet(
iconRes = R.drawable.rounded_person_24,
title = stringResource(R.string.island_notif_hub_chat_pictures),
isChecked = viewModel.isIslandNotifHubPreferChatPictures.value,
+ enabled = !(viewModel.isIslandNotifThemedBubble.value && themedAppIcons),
onCheckedChange = {
HapticUtil.performVirtualKeyHaptic(view)
viewModel.setIslandNotifHubPreferChatPictures(it)
},
)
+ IconToggleItem(
+ iconRes = R.drawable.rounded_invert_colors_24,
+ title = stringResource(R.string.island_notif_themed_bubble_title),
+ description = if (themedAppIcons) null else stringResource(R.string.island_notif_themed_bubble_needs_themed_icons),
+ isChecked = viewModel.isIslandNotifThemedBubble.value && themedAppIcons,
+ enabled = themedAppIcons,
+ onCheckedChange = {
+ HapticUtil.performVirtualKeyHaptic(view)
+ viewModel.setIslandNotifThemedBubble(it)
+ },
+ )
ConfigSliderItem(
title = stringResource(R.string.island_hide_icon_notifications),
value = viewModel.islandNotifHubIcons.intValue.toFloat(),
diff --git a/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt
index d79ac292d..c62bb035d 100644
--- a/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt
+++ b/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt
@@ -55,6 +55,19 @@ object AppIconUtil {
val icon = pm.getApplicationIcon(packageName)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return icon
if (!SettingsRepository(context).getBoolean(SettingsRepository.KEY_ISLAND_THEMED_APP_ICONS, false)) return icon
+ return themed(context, monochromeCandidate(pm, packageName, icon) ?: icon) ?: icon
+ }
+
+ fun monochromeAppIcon(context: Context, packageName: String): Bitmap? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return null
+ val pm = context.packageManager
+ val icon = runCatching { pm.getApplicationIcon(packageName) }.getOrNull() ?: return null
+ val adaptive = monochromeCandidate(pm, packageName, icon) ?: return null
+ return (themed(context, adaptive) as? BitmapDrawable)?.bitmap
+ }
+
+ @androidx.annotation.RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun monochromeCandidate(pm: PackageManager, packageName: String, icon: Drawable): Drawable? {
val launcher = android.content.Intent(android.content.Intent.ACTION_MAIN).addCategory(android.content.Intent.CATEGORY_LAUNCHER).setPackage(packageName)
val candidates = listOf(icon) + (
try {
@@ -63,8 +76,7 @@ object AppIconUtil {
emptyList()
}
)
- val withMonochrome = candidates.firstOrNull { (it as? AdaptiveIconDrawable)?.monochrome != null }
- return themed(context, withMonochrome ?: icon) ?: icon
+ return candidates.firstOrNull { (it as? AdaptiveIconDrawable)?.monochrome != null }
}
enum class ThemedMode { MONO, DUO }
diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
index 41e0e7596..ff8a101bc 100644
--- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
+++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt
@@ -303,6 +303,7 @@ class MainViewModel : ViewModel() {
val isIslandOtpEnabled = mutableStateOf(false)
val islandOtpTimeoutMs = mutableLongStateOf(60_000L)
val isIslandShowGlow = mutableStateOf(true)
+ val isIslandNotifThemedBubble = mutableStateOf(false)
val isIslandBorderOutline = mutableStateOf(false)
val islandBorderOutlineColor = mutableStateOf(SettingsRepository.ISLAND_BORDER_OUTLINE_DEFAULT_COLOR)
val isIslandBorderOutlineDynamic = mutableStateOf(false)
@@ -2596,6 +2597,7 @@ class MainViewModel : ViewModel() {
isIslandOtpEnabled.value = settingsRepository.isIslandOtpEnabled()
islandOtpTimeoutMs.longValue = settingsRepository.getIslandOtpTimeoutMs()
isIslandShowGlow.value = settingsRepository.isIslandShowGlowEnabled()
+ isIslandNotifThemedBubble.value = settingsRepository.isIslandNotifThemedBubbleEnabled()
isIslandBorderOutline.value = settingsRepository.isIslandBorderOutlineEnabled()
islandBorderOutlineColor.value = settingsRepository.getIslandBorderOutlineColor()
isIslandBorderOutlineDynamic.value = settingsRepository.isIslandBorderOutlineDynamicEnabled()
@@ -6199,6 +6201,11 @@ class MainViewModel : ViewModel() {
settingsRepository.setIslandShowGlowEnabled(enabled)
}
+ fun setIslandNotifThemedBubble(enabled: Boolean) {
+ isIslandNotifThemedBubble.value = enabled
+ settingsRepository.setIslandNotifThemedBubbleEnabled(enabled)
+ }
+
fun setIslandBorderOutline(enabled: Boolean) {
isIslandBorderOutline.value = enabled
settingsRepository.setIslandBorderOutlineEnabled(enabled)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e08d93c08..2c89a5122 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3231,6 +3231,8 @@
Glow spread
Pulse duration
App based color glow
+ App icon color bubbles
+ Turn on Use themed app icons in Visuals
Expanded width
Camera gap
Width when long pressed to expand