-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fabrizio.scarponi/111 spinning indicator circular progress indicator (#…
…134) * wip * wip * Add a customizable circular progress to the UI This commit adds a flexible circular progress option to the UI that supports both small and large size variants. The base theme has been updated to provide styling to both variants, and new SVG files have been added to provide dynamic views of the circular progress. The circular progress can be incorporated in the UI using the CircularProgress and CircularProgressBig components. This addition provides a more visually engaging way to show progress in the UI. * Refactor names of circular progress components and styles * wip * wip * Refactor circular progress styles * Remove unused spinner SVG files * Remove SvgLoader from CircularProgressStyle * Remove SvgLoader from CircularProgressStyle * Improve handling of non-existent colors Added a `FallbackMarker` in BridgeUtils.kt, improving the named color retrieval process. The `retrieveColorOrNull` method now checks for colors that are not found and returns a specific marker, rather than an inappropriate color. This avoids any confusion and enhances the overall color management within the application." * Update svgLoader usage for progress bars * fixes
- Loading branch information
Showing
15 changed files
with
466 additions
and
50 deletions.
There are no files selected for viewing
170 changes: 170 additions & 0 deletions
170
core/src/main/kotlin/org/jetbrains/jewel/CircularProgressIndicator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
package org.jetbrains.jewel | ||
|
||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.takeOrElse | ||
import androidx.compose.ui.unit.DpSize | ||
import androidx.compose.ui.unit.dp | ||
import kotlinx.coroutines.delay | ||
import org.jetbrains.jewel.styling.CircularProgressStyle | ||
import org.jetbrains.jewel.util.toHexString | ||
|
||
@Composable | ||
fun CircularProgressIndicator( | ||
svgLoader: SvgLoader, | ||
modifier: Modifier = Modifier, | ||
style: CircularProgressStyle = IntelliJTheme.circularProgressStyle, | ||
) { | ||
CircularProgressIndicatorImpl( | ||
modifier = modifier, | ||
svgLoader = svgLoader, | ||
iconSize = DpSize(16.dp, 16.dp), | ||
style = style, | ||
frameRetriever = { color -> SpinnerProgressIconGenerator.Small.generateSvgFrames(color.toHexString()) }, | ||
) | ||
} | ||
|
||
@Composable | ||
fun CircularProgressIndicatorBig( | ||
svgLoader: SvgLoader, | ||
modifier: Modifier = Modifier, | ||
style: CircularProgressStyle = IntelliJTheme.circularProgressStyle, | ||
) { | ||
CircularProgressIndicatorImpl( | ||
modifier = modifier, | ||
svgLoader = svgLoader, | ||
iconSize = DpSize(32.dp, 32.dp), | ||
style = style, | ||
frameRetriever = { color -> SpinnerProgressIconGenerator.Big.generateSvgFrames(color.toHexString()) }, | ||
) | ||
} | ||
|
||
@Composable | ||
private fun CircularProgressIndicatorImpl( | ||
modifier: Modifier = Modifier, | ||
svgLoader: SvgLoader, | ||
iconSize: DpSize, | ||
style: CircularProgressStyle, | ||
frameRetriever: (Color) -> List<String>, | ||
) { | ||
val defaultColor = if (IntelliJTheme.isDark) Color(0xFF6F737A) else Color(0xFFA8ADBD) | ||
var isFrameReady by remember { mutableStateOf(false) } | ||
var currentFrame: Pair<String, Int> by remember { mutableStateOf("" to 0) } | ||
|
||
if (!isFrameReady) { | ||
Box(modifier.size(iconSize)) | ||
} else { | ||
Icon( | ||
modifier = modifier.size(iconSize), | ||
painter = svgLoader.loadRawSvg( | ||
currentFrame.first, | ||
"circularProgressIndicator_frame_${currentFrame.second}", | ||
), | ||
contentDescription = null, | ||
) | ||
} | ||
|
||
LaunchedEffect(style.color) { | ||
val frames = frameRetriever(style.color.takeOrElse { defaultColor }) | ||
while (true) { | ||
for (i in 0 until frames.size) { | ||
currentFrame = frames[i] to i | ||
isFrameReady = true | ||
delay(style.frameTime.inWholeMilliseconds) | ||
} | ||
} | ||
} | ||
} | ||
|
||
object SpinnerProgressIconGenerator { | ||
|
||
private val opacityList = listOf(1.0f, 0.93f, 0.78f, 0.69f, 0.62f, 0.48f, 0.38f, 0.0f) | ||
|
||
private fun StringBuilder.closeRoot() = append("</svg>") | ||
private fun StringBuilder.openRoot(sizePx: Int) = append( | ||
"<svg width=\"$sizePx\" height=\"$sizePx\" viewBox=\"0 0 16 16\" fill=\"none\" " + | ||
"xmlns=\"http://www.w3.org/2000/svg\">", | ||
) | ||
|
||
private fun generateSvgIcon( | ||
size: Int, | ||
opacityListShifted: List<Float>, | ||
colorHex: String, | ||
) = | ||
buildString { | ||
openRoot(size) | ||
elements( | ||
colorHex = colorHex, | ||
opacityList = opacityListShifted, | ||
) | ||
closeRoot() | ||
} | ||
|
||
private fun StringBuilder.elements( | ||
colorHex: String, | ||
opacityList: List<Float>, | ||
) { | ||
append( | ||
"\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[0]}\" x=\"7\" y=\"1\" width=\"2\" height=\"4\" rx=\"1\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[1]}\" x=\"2.34961\" y=\"3.76416\" width=\"2\" height=\"4\" rx=\"1\"\n" + | ||
" transform=\"rotate(-45 2.34961 3.76416)\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[2]}\" x=\"1\" y=\"7\" width=\"4\" height=\"2\" rx=\"1\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[3]}\" x=\"5.17871\" y=\"9.40991\" width=\"2\" height=\"4\" rx=\"1\"\n" + | ||
" transform=\"rotate(45 5.17871 9.40991)\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[4]}\" x=\"7\" y=\"11\" width=\"2\" height=\"4\" rx=\"1\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[5]}\" x=\"9.41016\" y=\"10.8242\" width=\"2\" height=\"4\" rx=\"1\"\n" + | ||
" transform=\"rotate(-45 9.41016 10.8242)\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[6]}\" x=\"11\" y=\"7\" width=\"4\" height=\"2\" rx=\"1\"/>\n" + | ||
" <rect fill=\"$colorHex\" opacity=\"${opacityList[7]}\" x=\"12.2383\" y=\"2.3501\" width=\"2\" height=\"4\" rx=\"1\"\n" + | ||
" transform=\"rotate(45 12.2383 2.3501)\"/>\n", | ||
) | ||
} | ||
|
||
object Small { | ||
|
||
fun generateSvgFrames(colorHex: String) = buildList { | ||
val opacityListShifted = opacityList.toMutableList() | ||
repeat(opacityList.count()) { | ||
add( | ||
generateSvgIcon( | ||
size = 16, | ||
colorHex = colorHex, | ||
opacityListShifted = opacityListShifted, | ||
), | ||
) | ||
opacityListShifted.shtr() | ||
} | ||
} | ||
} | ||
|
||
object Big { | ||
|
||
fun generateSvgFrames(colorHex: String) = buildList { | ||
val opacityListShifted = opacityList.toMutableList() | ||
repeat(opacityList.count()) { | ||
add( | ||
generateSvgIcon( | ||
size = 32, | ||
colorHex = colorHex, | ||
opacityListShifted = opacityListShifted, | ||
), | ||
) | ||
opacityListShifted.shtr() | ||
} | ||
} | ||
} | ||
|
||
private fun <T> MutableList<T>.shtr() { | ||
add(first()) | ||
removeFirst() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
core/src/main/kotlin/org/jetbrains/jewel/styling/CircularProgressStyle.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package org.jetbrains.jewel.styling | ||
|
||
import androidx.compose.runtime.staticCompositionLocalOf | ||
import androidx.compose.ui.graphics.Color | ||
import kotlin.time.Duration | ||
|
||
interface CircularProgressStyle { | ||
|
||
val frameTime: Duration | ||
val color: Color | ||
} | ||
|
||
val LocalCircularProgressStyle = staticCompositionLocalOf<CircularProgressStyle> { | ||
error("No CircularProgressIndicatorStyle provided") | ||
} |
22 changes: 22 additions & 0 deletions
22
core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package org.jetbrains.jewel.util | ||
|
||
import androidx.compose.ui.graphics.Color | ||
import kotlin.math.roundToInt | ||
|
||
fun Color.toHexString(): String { | ||
val r = Integer.toHexString((red * 255).roundToInt()) | ||
val g = Integer.toHexString((green * 255).roundToInt()) | ||
val b = Integer.toHexString((blue * 255).roundToInt()) | ||
|
||
return buildString { | ||
append('#') | ||
append(r.padStart(2, '0')) | ||
append(g.padStart(2, '0')) | ||
append(b.padStart(2, '0')) | ||
|
||
if (alpha != 1.0f) { | ||
val a = Integer.toHexString((alpha * 255).roundToInt()) | ||
append(a.padStart(2, '0')) | ||
} | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
core/src/main/kotlin/org/jetbrains/jewel/util/SpinnerProgressIconGenerator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package org.jetbrains.jewel.util | ||
|
||
object SpinnerProgressIconGenerator { | ||
|
||
private val opacityList = listOf(1.0f, 0.93f, 0.78f, 0.69f, 0.62f, 0.48f, 0.38f, 0.0f) | ||
|
||
private val rotations = listOf(0, -45, 0, 45, 0, -45, 0, 45) | ||
|
||
// for a 16x16 icon | ||
internal val points = listOf( | ||
7f to 1f, | ||
2.34961f to 3.76416f, | ||
1f to 7f, | ||
5.17871f to 9.40991f, | ||
7f to 11f, | ||
9.41016f to 10.8242f, | ||
11f to 7f, | ||
12.2383f to 2.34961f, | ||
) | ||
|
||
private fun StringBuilder.closeTag() = append("</svg>") | ||
private fun StringBuilder.openTag(sizePx: Int) = append( | ||
"<svg width=\"$sizePx\" height=\"$sizePx\" viewBox=\"0 0 $sizePx $sizePx\" fill=\"none\" " + | ||
"xmlns=\"http://www.w3.org/2000/svg\">", | ||
) | ||
|
||
private fun getSvgPlainTextIcon( | ||
step: Int, | ||
pointList: List<Pair<Float, Float>>, | ||
colorHex: String, | ||
thickness: Int = 2, | ||
length: Int = 4, | ||
cornerRadius: Int = 1, | ||
) = | ||
buildString { | ||
openTag(16) | ||
appendLine() | ||
for (index in 0..opacityList.lastIndex) { | ||
val currentIndex = (index + step + 1) % opacityList.size | ||
val currentOpacity = opacityList[currentIndex] | ||
if (currentOpacity == 0.0f) continue | ||
drawElement( | ||
colorHex = colorHex, | ||
opacity = currentOpacity, | ||
x = pointList[index].first, | ||
y = pointList[index].second, | ||
width = thickness, | ||
height = length, | ||
rx = cornerRadius, | ||
rotation = rotations[index], | ||
) | ||
} | ||
closeTag() | ||
appendLine() | ||
} | ||
|
||
private fun StringBuilder.drawElement( | ||
colorHex: String, | ||
opacity: Float, | ||
x: Float, | ||
y: Float, | ||
width: Int, | ||
height: Int, | ||
rx: Int, | ||
rotation: Int, | ||
) { | ||
append("<rect fill=\"${colorHex}\" opacity=\"$opacity\" x=\"$x\" y=\"$y\" width=\"$width\" height=\"$height\" rx=\"$rx\"") | ||
if (rotation != 0) append(" transform=\"rotate($rotation $x $y)\"") | ||
append("/>\n") | ||
} | ||
|
||
internal fun getPlainTextSvgList(colorHex: String, size: Int) = buildList { | ||
val scaleFactor = size / 16f | ||
for (index in 0..opacityList.lastIndex) { | ||
if (size == 16) { | ||
add(getSvgPlainTextIcon(index, points, colorHex)) | ||
} else { | ||
add( | ||
getSvgPlainTextIcon( | ||
index, | ||
points.map { it.first * scaleFactor to it.second * scaleFactor }, | ||
colorHex, | ||
thickness = (2 * scaleFactor).toInt().coerceAtLeast(1), | ||
length = (4 * scaleFactor).toInt().coerceAtLeast(1), | ||
cornerRadius = (2 * scaleFactor).toInt().coerceAtLeast(1), | ||
), | ||
) | ||
} | ||
} | ||
} | ||
|
||
object Small { | ||
|
||
fun generateRawSvg(colorHex: String) = getPlainTextSvgList(colorHex = colorHex, size = 16) | ||
} | ||
|
||
object Big { | ||
|
||
fun generateRawSvg(colorHex: String) = | ||
getPlainTextSvgList(colorHex = colorHex, size = 32) | ||
} | ||
} |
Oops, something went wrong.