From c2c72ecfe1339951ac2865e9158c13697424f19e Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 18 Jun 2023 17:04:22 +0200 Subject: [PATCH] feat: support linear gradients in WebCompat.parseColour rename it to ColourCompat already Signed-off-by: Infi --- .../chat/revolt/api/internals/WebCompat.kt | 174 ++++++++++++++++-- 1 file changed, 158 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/chat/revolt/api/internals/WebCompat.kt b/app/src/main/java/chat/revolt/api/internals/WebCompat.kt index 4cd220b3..cb0e6423 100644 --- a/app/src/main/java/chat/revolt/api/internals/WebCompat.kt +++ b/app/src/main/java/chat/revolt/api/internals/WebCompat.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Brush.Companion.linearGradient import androidx.compose.ui.graphics.Color // color is spelled american because Color from compose is spelled american @@ -17,28 +18,169 @@ fun Brush.Companion.solidColor(colour: Color) = linearGradient( object WebCompat { @Composable - fun parseColour(colour: String): Brush { - if (colour.startsWith("var(")) { + private fun parseLinearGradient(gradient: String): Brush { + val stops = mutableListOf>() + + val parts = mutableListOf() + var startIndex = 0 + var openParenthesesCount = 0 + + for (i in gradient.indices) { + when (gradient[i]) { + '(' -> openParenthesesCount++ + ')' -> openParenthesesCount-- + ',' -> { + if (openParenthesesCount == 0) { + val part = gradient.substring(startIndex, i).trim() + parts.add(part) + startIndex = i + 1 + } + } + } + } + + val lastPart = gradient.substring(startIndex).trim() + if (lastPart.isNotEmpty()) { + parts.add(lastPart) + } + + parts.forEachIndexed { index, part -> + if (part.startsWith("to") || part.endsWith("deg")) { + // we don't support any other direction / blocked on compose supporting them + // TODO could probably emulate this by swapping the values around + } else { + val splitPart = part.split(" ") + + val colourPart = splitPart[0] + val colour = when { + colourPart.startsWith("var(") -> { + parseVarToColour( + colourPart.substringAfter("var(").substringBeforeLast(")") + ) + } + + else -> parseFunctionColour(colourPart) ?: try { + Color(android.graphics.Color.parseColor(colourPart)) + } catch (e: IllegalArgumentException) { + Log.d( + "WebCompat", + "Failed to parse colour $colourPart in $gradient, falling back to LocalContentColor.current" + ) + LocalContentColor.current + } + } + + val stop = if (splitPart.size == 2) { + splitPart[1].removeSuffix("%").toFloat() / 100f + } else { + index.toFloat() / (parts.size - 1) + } + + stops.add(stop to colour) + } + } + + return linearGradient( + colorStops = stops.toTypedArray() + ) + } + + @Composable + private fun parseFunctionColour(colourString: String): Color? { + val cleanedString = colourString.trim() + + return try { + if (cleanedString.startsWith("rgb(")) { + parseRGBColour(cleanedString) + } else if (cleanedString.startsWith("rgba(")) { + parseRGBAColour(cleanedString) + } else { + throw IllegalArgumentException("Invalid colour format: $colourString") + } + } catch (e: Exception) { Log.d( "WebCompat", - "Parsing colour $colour. ${colour.substringAfter("var(").substringBefore(")")}" + "Failed to parse colour $colourString, falling back to LocalContentColor.current" ) - return when (colour.substringAfter("var(").substringBefore(")")) { - "--accent" -> Brush.solidColor(MaterialTheme.colorScheme.primary) - "--foreground" -> Brush.solidColor(MaterialTheme.colorScheme.onBackground) - "--background" -> Brush.solidColor(MaterialTheme.colorScheme.background) - "--error" -> Brush.solidColor(MaterialTheme.colorScheme.error) - else -> Brush.solidColor(LocalContentColor.current) - } - } else { - try { - return Brush.solidColor(Color(android.graphics.Color.parseColor(colour))) - } catch (e: IllegalArgumentException) { + null + } + } + + private fun parseRGBColour(rgbString: String): Color { + val colourParts = rgbString.removePrefix("rgb(") + .removeSuffix(")") + .split(",") + .map { it.trim().toInt() } + + val red = colourParts[0] / 255.0f + val green = colourParts[1] / 255.0f + val blue = colourParts[2] / 255.0f + + return Color(red, green, blue) + } + + private fun parseRGBAColour(rgbaString: String): Color { + val colourParts = rgbaString.removePrefix("rgba(") + .removeSuffix(")") + .split(",") + .map { it.trim() } + + val red = colourParts[0].toInt() / 255.0f + val green = colourParts[1].toInt() / 255.0f + val blue = colourParts[2].toInt() / 255.0f + val alpha = colourParts[3].removeSuffix("%").toFloat() / 100.0f + + return Color(red, green, blue, alpha) + } + + @Composable + private fun parseVarToColour(varName: String): Color { + return when (varName) { + "--accent" -> MaterialTheme.colorScheme.primary + "--foreground" -> MaterialTheme.colorScheme.onBackground + "--background" -> MaterialTheme.colorScheme.background + "--error" -> MaterialTheme.colorScheme.error + else -> LocalContentColor.current + } + } + + @Composable + private fun parseVar(varName: String): Brush { + return Brush.solidColor(parseVarToColour(varName)) + } + + @Composable + fun parseColour(colour: String): Brush { + when { + colour.startsWith("var(") -> { Log.d( "WebCompat", - "Failed to parse colour $colour, falling back to LocalContentColor.current" + "Parsing variable $colour" ) - return Brush.solidColor(LocalContentColor.current) + return parseVar( + colour.substringAfter("var(").substringBeforeLast(")") + ) + } + + colour.startsWith("linear-gradient(") || colour.startsWith("repeating-linear-gradient(") -> { + return parseLinearGradient( + colour + .substringAfter("repeating-") + .substringAfter("linear-gradient(") + .substringBeforeLast(")") + ) + } + + else -> { + return try { + Brush.solidColor(Color(android.graphics.Color.parseColor(colour))) + } catch (e: IllegalArgumentException) { + Log.d( + "WebCompat", + "Failed to parse colour $colour, falling back to LocalContentColor.current" + ) + Brush.solidColor(LocalContentColor.current) + } } } }