Skip to content

Commit b9e325b

Browse files
committed
docs(native-access): enhance screen capture example with coroutine support
- Refactor `ScreenCapture` API to use `suspend` for proper coroutine interoperability. - Add `ScreenshotViewer` Compose example for capturing and displaying the screenshot asynchronously. - Update `native-access.md` with new usage patterns and Compose example to illustrate integration.
1 parent 029ee48 commit b9e325b

1 file changed

Lines changed: 81 additions & 61 deletions

File tree

docs/native-access.md

Lines changed: 81 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -67,81 +67,101 @@ That's the entire configuration. The plugin handles compilation, bundling, and l
6767

6868
Here's a real-world example: capturing the screen using macOS's CoreGraphics API. This is a platform API with no JVM equivalent — the kind of thing that would normally require JNI C glue.
6969

70-
**Native side** (`src/nativeMain/kotlin/com/example/screen/ScreenCapture.kt`):
70+
**Native side** (`src/nativeMain/kotlin/com/example/screen/SystemDesktop.kt`):
7171

7272
```kotlin
73-
package com.example.screen
74-
75-
import kotlinx.cinterop.*
76-
import platform.CoreFoundation.*
77-
import platform.CoreGraphics.*
78-
79-
class ScreenCapture {
80-
81-
fun capture(): ByteArray {
82-
val image = CGWindowListCreateImage(
83-
CGRectInfinite,
84-
kCGWindowListOptionOnScreenOnly,
85-
kCGNullWindowID,
86-
kCGWindowImageDefault
87-
) ?: return ByteArray(0)
88-
89-
val provider = CGImageGetDataProvider(image)
90-
val data = CGDataProviderCopyData(provider)
91-
92-
val bytes = CFDataGetBytePtr(data)
93-
val length = CFDataGetLength(data).toInt()
94-
val result = ByteArray(length) { bytes!![it] }
95-
96-
CFRelease(data)
97-
CGImageRelease(image)
98-
return result
73+
// suspend — runs off the main thread, returns PNG bytes
74+
actual suspend fun captureScreen(): ByteArray = memScoped {
75+
if (!CGPreflightScreenCaptureAccess()) {
76+
CGRequestScreenCaptureAccess()
77+
return@memScoped ByteArray(0)
9978
}
10079

101-
fun width(): Int {
102-
val image = CGWindowListCreateImage(
103-
CGRectInfinite,
104-
kCGWindowListOptionOnScreenOnly,
105-
kCGNullWindowID,
106-
kCGWindowImageDefault
107-
) ?: return 0
108-
val w = CGImageGetWidth(image).toInt()
109-
CGImageRelease(image)
110-
return w
80+
val rect = alloc<CGRect>().apply {
81+
origin.x = CGRectInfinite.origin.x
82+
origin.y = CGRectInfinite.origin.y
83+
size.width = CGRectInfinite.size.width
84+
size.height = CGRectInfinite.size.height
11185
}
112-
113-
fun height(): Int {
114-
val image = CGWindowListCreateImage(
115-
CGRectInfinite,
116-
kCGWindowListOptionOnScreenOnly,
117-
kCGNullWindowID,
118-
kCGWindowImageDefault
119-
) ?: return 0
120-
val h = CGImageGetHeight(image).toInt()
121-
CGImageRelease(image)
122-
return h
86+
val cgImage = CGWindowListCreateImage(
87+
rect.readValue(),
88+
kCGWindowListOptionOnScreenOnly,
89+
kCGNullWindowID,
90+
kCGWindowImageDefault,
91+
) ?: return@memScoped ByteArray(0)
92+
93+
// Encode as PNG — NSBitmapImageRep handles all the pixel format details
94+
val bitmapRep = NSBitmapImageRep(cGImage = cgImage)
95+
CGImageRelease(cgImage)
96+
97+
val pngData = bitmapRep.representationUsingType(
98+
NSBitmapImageFileTypePNG,
99+
properties = emptyMap<Any?, Any>(),
100+
) ?: return@memScoped ByteArray(0)
101+
102+
ByteArray(pngData.length.toInt()) { i ->
103+
(pngData.bytes!!.reinterpret<ByteVar>() + i)!!.pointed.value
123104
}
124105
}
125106
```
126107

127-
**JVM side** — the plugin generates the proxy, you just use it:
108+
**JVM + Compose side** — the plugin generates the proxy, you just use it:
128109

129110
```kotlin
130-
import com.example.screen.ScreenCapture
131-
132-
fun main() {
133-
val capture = ScreenCapture()
134-
135-
val pixels = capture.capture() // ByteArray, straight from CoreGraphics
136-
val w = capture.width()
137-
val h = capture.height()
138-
139-
println("Captured ${w}×${h} pixels (${pixels.size} bytes)")
140-
capture.close() // explicit release (or auto-GC'd)
111+
import androidx.compose.foundation.Image
112+
import androidx.compose.foundation.layout.*
113+
import androidx.compose.material.Button
114+
import androidx.compose.material.Text
115+
import androidx.compose.runtime.*
116+
import androidx.compose.ui.Modifier
117+
import androidx.compose.ui.graphics.ImageBitmap
118+
import androidx.compose.ui.graphics.toComposeImageBitmap
119+
import androidx.compose.ui.layout.ContentScale
120+
import androidx.compose.ui.unit.dp
121+
import com.example.screen.SystemDesktop
122+
import kotlinx.coroutines.launch
123+
import org.jetbrains.skia.Image as SkiaImage
124+
125+
@Composable
126+
fun ScreenshotViewer() {
127+
val desktop = remember { SystemDesktop() }
128+
var bitmap by remember { mutableStateOf<ImageBitmap?>(null) }
129+
var capturing by remember { mutableStateOf(false) }
130+
val scope = rememberCoroutineScope()
131+
132+
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
133+
Button(
134+
onClick = {
135+
capturing = true
136+
scope.launch {
137+
val bytes = desktop.captureScreen() // suspend — UI never blocks
138+
if (bytes.isNotEmpty()) {
139+
bitmap = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
140+
}
141+
capturing = false
142+
}
143+
},
144+
enabled = !capturing,
145+
) {
146+
Text(if (capturing) "Capturing…" else "Capture Screen")
147+
}
148+
149+
bitmap?.let {
150+
Image(
151+
bitmap = it,
152+
contentDescription = "Screenshot",
153+
contentScale = ContentScale.FillWidth,
154+
modifier = Modifier.fillMaxWidth(),
155+
)
156+
}
157+
}
141158
}
142159
```
143160

144-
**No C. No JNI headers. No build scripts. No `System.loadLibrary` call.** The `.dylib` is compiled by the plugin, bundled in the JAR, and extracted automatically at runtime.
161+
**No C. No JNI headers. No build scripts. No `System.loadLibrary` call.** The `.dylib` is compiled by the plugin, bundled in the JAR, and extracted automatically at runtime. The `suspend` on the native side maps transparently to a coroutine on the JVM — the UI stays responsive while CoreGraphics does the work.
162+
163+
!!! tip "Full working example"
164+
The [systeminfo example](https://github.com/kdroidFilter/NucleusNativeAccess/tree/main/examples/systeminfo) in the NucleusNativeAccess repo implements this pattern for all three platforms (CoreGraphics on macOS, XDG ScreenCast + PipeWire on Linux, GDI on Windows), plus native notifications, a system tray menu, and real-time memory updates via `Flow`.
145165

146166
The same pattern works for any other platform API:
147167

0 commit comments

Comments
 (0)