Skip to content

Commit acfe824

Browse files
committed
Refactor ComposableIconUtils to remove rendering API fallback logic for simplification.
1 parent cf577d0 commit acfe824

1 file changed

Lines changed: 43 additions & 161 deletions

File tree

src/commonMain/kotlin/com/kdroid/composetray/utils/ComposableIconUtils.kt

Lines changed: 43 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ package com.kdroid.composetray.utils
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.ui.ImageComposeScene
5+
import androidx.compose.ui.use
56
import kotlinx.coroutines.Dispatchers
6-
import org.jetbrains.skia.*
7+
import org.jetbrains.skia.Bitmap
8+
import org.jetbrains.skia.EncodedImageFormat
9+
import org.jetbrains.skia.FilterMipmap
10+
import org.jetbrains.skia.FilterMode
11+
import org.jetbrains.skia.Image
12+
import org.jetbrains.skia.MipmapMode
713
import java.io.File
814
import java.util.zip.CRC32
915

@@ -12,156 +18,35 @@ import java.util.zip.CRC32
1218
*/
1319
object ComposableIconUtils {
1420

15-
private var currentRenderApi: RenderApi = RenderApi.AUTO
16-
17-
enum class RenderApi {
18-
AUTO,
19-
DIRECTX12,
20-
OPENGL,
21-
SOFTWARE
22-
}
23-
24-
init {
25-
// Start with automatic detection, will fallback if needed
26-
configureRenderingApi(RenderApi.AUTO)
27-
}
28-
29-
/**
30-
* Configure Skiko rendering API with fallback mechanism
31-
*/
32-
private fun configureRenderingApi(api: RenderApi) {
33-
try {
34-
when (api) {
35-
RenderApi.AUTO -> {
36-
// Let Skiko choose automatically (usually tries DirectX12 first on Windows)
37-
System.clearProperty("skiko.renderApi")
38-
debugln { "[ComposableIconUtils] Using automatic rendering API selection" }
39-
}
40-
RenderApi.DIRECTX12 -> {
41-
System.setProperty("skiko.renderApi", "DIRECT3D")
42-
debugln { "[ComposableIconUtils] Configured DirectX12 rendering" }
43-
}
44-
RenderApi.OPENGL -> {
45-
System.setProperty("skiko.renderApi", "OPENGL")
46-
System.setProperty("skiko.rendering.api", "OPENGL")
47-
debugln { "[ComposableIconUtils] Configured OpenGL rendering" }
48-
}
49-
RenderApi.SOFTWARE -> {
50-
System.setProperty("skiko.renderApi", "SOFTWARE")
51-
System.setProperty("skiko.softwareRendering", "true")
52-
System.setProperty("skiko.rendering.api", "SOFTWARE")
53-
System.setProperty("skiko.rendering.useGpu", "false")
54-
System.setProperty("skiko.directx.disabled", "true")
55-
debugln { "[ComposableIconUtils] Configured software rendering" }
56-
}
57-
}
58-
currentRenderApi = api
59-
} catch (e: Exception) {
60-
errorln { "[ComposableIconUtils] Failed to configure rendering API: ${e.message}" }
61-
}
62-
}
63-
64-
/**
65-
* Try the next rendering API in the fallback chain
66-
*/
67-
private fun tryNextRenderApi(): Boolean {
68-
return when (currentRenderApi) {
69-
RenderApi.AUTO, RenderApi.DIRECTX12 -> {
70-
debugln { "[ComposableIconUtils] Falling back from $currentRenderApi to OpenGL" }
71-
configureRenderingApi(RenderApi.OPENGL)
72-
true
73-
}
74-
RenderApi.OPENGL -> {
75-
debugln { "[ComposableIconUtils] Falling back from OpenGL to Software rendering" }
76-
configureRenderingApi(RenderApi.SOFTWARE)
77-
true
78-
}
79-
RenderApi.SOFTWARE -> {
80-
errorln { "[ComposableIconUtils] Already using software rendering, no more fallbacks available" }
81-
false
82-
}
83-
}
84-
}
85-
8621
/**
8722
* Renders a Composable to a PNG file and returns the path to the file.
8823
*
8924
* @param iconRenderProperties Properties for rendering the icon
9025
* @param content The Composable content to render
9126
* @return Path to the generated PNG file
92-
* @throws Exception if rendering fails after all fallback attempts
27+
* @throws Exception if rendering fails completely
9328
*/
9429
fun renderComposableToPngFile(
9530
iconRenderProperties: IconRenderProperties,
9631
content: @Composable () -> Unit
9732
): String {
9833
val tempFile = createTempFile(suffix = ".png")
99-
val pngData = renderComposableToPngBytesWithFallback(iconRenderProperties, content)
34+
val pngData = renderComposableToPngBytes(iconRenderProperties, content)
10035
tempFile.writeBytes(pngData)
10136
return tempFile.absolutePath
10237
}
10338

104-
/**
105-
* Renders a Composable to PNG bytes with automatic fallback between rendering APIs
106-
*/
107-
private fun renderComposableToPngBytesWithFallback(
108-
iconRenderProperties: IconRenderProperties,
109-
content: @Composable () -> Unit
110-
): ByteArray {
111-
var attempts = 0
112-
val maxAttempts = 3 // Try DirectX/Auto, then OpenGL, then Software
113-
var lastException: Exception? = null
114-
115-
while (attempts < maxAttempts) {
116-
try {
117-
debugln { "[ComposableIconUtils] Render attempt ${attempts + 1} with $currentRenderApi" }
118-
return renderComposableToPngBytes(iconRenderProperties, content)
119-
} catch (e: Exception) {
120-
lastException = e
121-
val errorMessage = e.message ?: ""
122-
val errorClassName = e.javaClass.simpleName
123-
124-
errorln { "[ComposableIconUtils] $errorClassName with ${currentRenderApi}: $errorMessage" }
125-
126-
// Check if the error is related to DirectX, OpenGL, or rendering in general
127-
val isDirectXError = errorMessage.contains("DirectX12", ignoreCase = true) ||
128-
errorMessage.contains("D3D12", ignoreCase = true) ||
129-
errorMessage.contains("Direct3D", ignoreCase = true) ||
130-
errorMessage.contains("choose DirectX12 adapter", ignoreCase = true) ||
131-
errorClassName.contains("RenderException", ignoreCase = true)
132-
133-
val isOpenGLError = errorMessage.contains("OpenGL", ignoreCase = true) ||
134-
errorMessage.contains("GL", ignoreCase = true)
135-
136-
val isRenderingError = errorClassName.contains("Render", ignoreCase = true) ||
137-
errorMessage.contains("render", ignoreCase = true)
138-
139-
// If it's a rendering-related error, try the next API
140-
if (isDirectXError || isOpenGLError || isRenderingError) {
141-
if (tryNextRenderApi()) {
142-
attempts++
143-
continue // Try again with the next API
144-
}
145-
}
146-
147-
// If we can't identify the error or no more fallbacks, throw the exception
148-
break
149-
}
150-
}
151-
152-
// If all attempts failed, throw the last exception
153-
throw lastException ?: Exception("Failed to render composable after $attempts attempts")
154-
}
155-
15639
/**
15740
* Renders a Composable to a PNG image and returns the result as a byte array.
41+
* This function creates an [ImageComposeScene] based on the provided [IconRenderProperties],
42+
* renders the Composable content, and encodes the output into PNG format.
43+
* If scaling is required based on the [IconRenderProperties], the rendered content is scaled before encoding.
15844
*
15945
* @param iconRenderProperties Properties for rendering the icon
16046
* @param content The Composable content to render
16147
* @return A byte array containing the rendered PNG image data.
16248
* @throws Exception if rendering fails
16349
*/
164-
@Throws(Exception::class)
16550
fun renderComposableToPngBytes(
16651
iconRenderProperties: IconRenderProperties,
16752
content: @Composable () -> Unit
@@ -172,18 +57,32 @@ object ComposableIconUtils {
17257
var scaledImage: Image? = null
17358

17459
try {
175-
// Create the scene - this is where DirectX/OpenGL errors typically occur
176-
scene = ImageComposeScene(
177-
width = iconRenderProperties.sceneWidth,
178-
height = iconRenderProperties.sceneHeight,
179-
density = iconRenderProperties.sceneDensity,
180-
coroutineContext = Dispatchers.Unconfined
181-
) {
182-
content()
183-
}
60+
// Try to create and render the scene
61+
try {
62+
scene = ImageComposeScene(
63+
width = iconRenderProperties.sceneWidth,
64+
height = iconRenderProperties.sceneHeight,
65+
density = iconRenderProperties.sceneDensity,
66+
coroutineContext = Dispatchers.Unconfined
67+
) {
68+
content()
69+
}
70+
71+
renderedIcon = scene.render()
72+
} catch (e: Exception) {
73+
// Log the error but don't modify any system properties
74+
val errorMessage = e.message ?: "Unknown error"
75+
errorln { "[ComposableIconUtils] Failed to render scene: $errorMessage" }
76+
77+
// Check if it's a DirectX error on Windows
78+
if (errorMessage.contains("DirectX12", ignoreCase = true) ||
79+
errorMessage.contains("Failed to choose DirectX12 adapter", ignoreCase = true)) {
80+
errorln { "[ComposableIconUtils] DirectX12 not available on this system. Scene rendering failed." }
81+
}
18482

185-
// Render the scene - this may also trigger rendering API errors
186-
renderedIcon = scene.render()
83+
// Re-throw the exception - let the caller handle it
84+
throw e
85+
}
18786

18887
val iconData = if (iconRenderProperties.requiresScaling) {
18988
scaledBitmap = Bitmap().apply {
@@ -205,9 +104,6 @@ object ComposableIconUtils {
205104
}
206105

207106
return iconData.bytes
208-
} catch (e: Exception) {
209-
// Re-throw to be handled by the fallback wrapper
210-
throw e
211107
} finally {
212108
// Ensure proper cleanup
213109
try {
@@ -227,7 +123,7 @@ object ComposableIconUtils {
227123
* @param iconRenderProperties Properties for rendering the icon
228124
* @param content The Composable content to render
229125
* @return Path to the generated ICO file
230-
* @throws Exception if rendering fails after all fallback attempts
126+
* @throws Exception if rendering fails
231127
*/
232128
fun renderComposableToIcoFile(
233129
iconRenderProperties: IconRenderProperties,
@@ -247,14 +143,14 @@ object ComposableIconUtils {
247143
* @param iconRenderProperties Properties for rendering the icon
248144
* @param content The Composable content to render
249145
* @return Byte array containing the ICO data
250-
* @throws Exception if rendering fails after all fallback attempts
146+
* @throws Exception if rendering fails
251147
*/
252148
fun renderComposableToIcoBytes(
253149
iconRenderProperties: IconRenderProperties,
254150
content: @Composable () -> Unit
255151
): ByteArray {
256-
// First render to PNG format (with fallback support)
257-
val pngBytes = renderComposableToPngBytesWithFallback(iconRenderProperties, content)
152+
// First render to PNG format (which is supported)
153+
val pngBytes = renderComposableToPngBytes(iconRenderProperties, content)
258154

259155
// Create a simple ICO format wrapper around the PNG data
260156
// ICO header (6 bytes) + ICO directory entry (16 bytes) + PNG data
@@ -300,20 +196,6 @@ object ComposableIconUtils {
300196
return icoData
301197
}
302198

303-
/**
304-
* Resets the rendering API to try again from the beginning of the fallback chain.
305-
* Useful when creating new instances or after configuration changes.
306-
*/
307-
fun resetRenderingApi() {
308-
debugln { "[ComposableIconUtils] Resetting rendering API to AUTO" }
309-
configureRenderingApi(RenderApi.AUTO)
310-
}
311-
312-
/**
313-
* Gets the current rendering API being used
314-
*/
315-
fun getCurrentRenderApi(): String = currentRenderApi.name
316-
317199
/**
318200
* Creates a temporary file that will be deleted when the JVM exits.
319201
*/
@@ -337,8 +219,8 @@ object ComposableIconUtils {
337219
content: @Composable () -> Unit
338220
): Long {
339221
return try {
340-
// Render the composable to PNG bytes (with fallback support)
341-
val pngBytes = renderComposableToPngBytesWithFallback(iconRenderProperties, content)
222+
// Render the composable to PNG bytes
223+
val pngBytes = renderComposableToPngBytes(iconRenderProperties, content)
342224

343225
// Calculate CRC32 hash of the PNG bytes
344226
val crc = CRC32()

0 commit comments

Comments
 (0)