diff --git a/libui-compose/build.gradle.kts b/libui-compose/build.gradle.kts new file mode 100644 index 00000000..1041d625 --- /dev/null +++ b/libui-compose/build.gradle.kts @@ -0,0 +1,45 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") version "1.2.1" +} + +val os = org.gradle.internal.os.OperatingSystem.current()!! +val isRunningInIde: Boolean = System.getProperty("idea.active") == "true" + +kotlin { + if (os.isWindows) mingwX64("windows") + if (os.isLinux) linuxX64("linux") + if (os.isMacOsX) macosX64("macosx") + + sourceSets { + commonMain { + dependencies { + implementation(kotlin("stdlib-common")) + implementation(compose.runtime) + } + } + commonTest { + kotlin.srcDir("src/unitTest/kotlin") + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } + + targets.withType { + sourceSets["${targetName}Main"].apply { + kotlin.srcDir("src/nativeMain/kotlin") + dependencies { + api(project(":libui")) + } + } + + binaries { + executable(listOf(RELEASE)) { + } + } + } +} diff --git a/libui-compose/src/nativeMain/kotlin/Main.kt b/libui-compose/src/nativeMain/kotlin/Main.kt new file mode 100644 index 00000000..891916ea --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/Main.kt @@ -0,0 +1,62 @@ +import androidx.compose.runtime.* +import kotlinx.coroutines.* +import libui.compose.* +import libui.uiQuit + +fun main() { + println("HELLOOOOOO!!!") + + runLibUI { + val state = remember { WindowState(SizeInt(100, 100)) } + + Window( + onCloseRequest = { + println("CLOSE REQUEST!!!") + uiQuit() + }, + state = state, + title = "LibUI and Compose!", + content = { + var num by remember { mutableStateOf(1) } + val isOn = remember { mutableStateOf(true) } + + VBox { + Label("I did something here! $num") + Label("Look at this thing go ${num + 20}") + + Checkbox("Enable disappearing label", isOn) + if (!isOn.value || num % 4 == 0) { + Label("I like to have stuff in here") + } + HorizontalSeparator() + + Button("Click Me!", onClick = { println("You clicked me!") }) + + VerticalSeparator() + + ProgressBar() + ProgressBar(value = num % 100) + + val color = remember { mutableStateOf(Color(0xFFFFFF)) } + ColorButton(color) + + val simpleText = remember { mutableStateOf("Type here...") } + TextField(simpleText) + PasswordField(simpleText) + + HorizontalSeparator() + + val slide = remember { mutableStateOf(5) } + Slider(slide, 1, 20 + num / 10) + } + + LaunchedEffect(Unit) { + while (isActive) { + delay(1000) + num += 1 + } + } + } + ) + } +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Box.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Box.kt new file mode 100644 index 00000000..bda6aba3 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Box.kt @@ -0,0 +1,64 @@ +@file:Suppress("FunctionName") + +package libui.compose + +import androidx.compose.runtime.Applier +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import cnames.structs.uiBox +import kotlinx.cinterop.CPointer +import libui.* + +@Composable +fun VBox( + padded: Boolean = true, + enabled: Boolean = true, + visible: Boolean = true, + content: @Composable () -> Unit +) { + Box(ctor = { uiNewVerticalBox()!! }, padded, enabled, visible, content) +} + +@Composable +fun HBox( + padded: Boolean = true, + enabled: Boolean = true, + visible: Boolean = true, + content: @Composable () -> Unit +) { + Box(ctor = { uiNewHorizontalBox()!! }, padded, enabled, visible, content) +} + +@Composable +private fun Box( + ctor: () -> CPointer, + padded: Boolean, + enabled: Boolean, + visible: Boolean, + content: @Composable () -> Unit +) { + val control = rememberControl { ctor() } + + handleChildren(content) { BoxApplier(control.ptr) } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + set(padded) { uiBoxSetPadded(this, if (it) 1 else 0) } + } + ) +} + +class BoxApplier( + private val box: CPointer, +) : AppendDeleteApplier() { + override fun deleteItem(index: Int) { + uiBoxDelete(box, index) + } + + override fun appendItem(instance: CPointer?) { + val isStretchy = false + uiBoxAppend(box, instance, if (isStretchy) 1 else 0) + } +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Entry.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Entry.kt new file mode 100644 index 00000000..c3b9f180 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Entry.kt @@ -0,0 +1,103 @@ +@file:Suppress("FunctionName") + +package libui.compose + +import cnames.structs.uiWindow +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.Snapshot +import kotlinx.cinterop.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import libui.* +import kotlin.coroutines.CoroutineContext + +fun runLibUI(content: @Composable WindowScope.() -> Unit) = withLibUI { + runBlocking { + @OptIn(ExperimentalStdlibApi::class) + val uiDispatcher = LibUiDispatcher(coroutineContext[CoroutineDispatcher.Key]!!) + + val clock = BroadcastFrameClock() + + val scope = CoroutineScope(clock + uiDispatcher) + + scope.launch { + while (isActive) { + clock.sendFrame(0L) // Frame time value is not used by Compose runtime. + delay(50) + } + } + + Snapshot.globalWrites() + .conflate() + .onEach { Snapshot.sendApplyNotifications() } + .launchIn(scope) + + val recomposer = Recomposer(scope.coroutineContext) + // Will be cancelled when recomposerJob cancels + scope.launch { recomposer.runRecomposeAndApplyChanges() } + + composeLibUI(recomposer, content) { + uiMain() + uiDispatcher.close() + } + + recomposer.close() + recomposer.join() + + scope.cancel() + } +} + +private inline fun composeLibUI( + parent: CompositionContext, + noinline content: @Composable WindowScope.() -> Unit, + block: () -> Unit +) { + val applier = MutableListApplier>(mutableListOf()) + val composition = Composition(applier, parent) + composition.setContent { WindowScope().content() } + + block() + + // Free libui controls + composition.dispose() +} + +private class LibUiDispatcher(private val backup: CoroutineDispatcher) : CloseableCoroutineDispatcher() { + private var isClosed: Boolean = false + + override fun close() { + // There's a race condition between close() and dispatch() to fix. + isClosed = true + +// val onShouldQuit = { println("QUIT!") } +// val lol = StableRef.create(onShouldQuit) +// try { +// uiOnShouldQuit( +// staticCFunction { senderData -> +// 1 +// }, +// lol.asCPointer() +// ) +// } finally { +// lol.dispose() +// } + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + if (!isClosed) { + val stableRef = StableRef.create(block) + uiQueueMain( + staticCFunction { ptr -> + val ref = ptr!!.asStableRef() + val runnable = ref.get() + ref.dispose() + runnable.run() + }, + stableRef.asCPointer() + ) + } else { + backup.dispatch(context, block) + } + } +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Form.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Form.kt new file mode 100644 index 00000000..9627fb59 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Form.kt @@ -0,0 +1,42 @@ +package libui.compose + +import androidx.compose.runtime.Applier +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import cnames.structs.uiForm +import kotlinx.cinterop.CPointer +import libui.* + + +@Composable +fun Form( + padded: Boolean = true, + enabled: Boolean = true, + visible: Boolean = true, + content: @Composable () -> Unit +) { + val control = rememberControl { uiNewForm()!! } + + handleChildren(content) { FormApplier(control.ptr) } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + set(padded) { uiFormSetPadded(this, if (it) 1 else 0) } + } + ) +} + + +class FormApplier(private val form: CPointer) : AppendDeleteApplier() { + override fun appendItem(instance: CPointer?) { + val label = "" + val isStretchy = false + uiFormAppend(form, label, instance, if (isStretchy) 1 else 0) + } + + override fun deleteItem(index: Int) { + uiFormDelete(form, index) + } +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Layouts.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Layouts.kt new file mode 100644 index 00000000..bc43dc03 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Layouts.kt @@ -0,0 +1,59 @@ +@file:Suppress("FunctionName") + +package libui.compose + +import androidx.compose.runtime.* +import cnames.structs.uiGroup +//import cnames.structs.uiGrid +import kotlinx.cinterop.CPointer +import libui.* + +@Composable +fun Group( + title: String, + margined: Boolean = false, + enabled: Boolean = true, + visible: Boolean = true, + content: @Composable () -> Unit +) { + val control = rememberControl { uiNewGroup(title)!! } + + handleChildren(content) { + object : SingletonApplier>() { + override fun setItem(item: CPointer?) { + uiGroupSetChild(control.ptr, item) + } + } + } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + update(title) { uiGroupSetTitle(this, it) } + set(margined) { uiGroupSetMargined(this, if (it) 1 else 0) } + } + ) +} + +//@Composable +//fun Grid( +// padded: Boolean = true, +// enabled: Boolean = true, +// visible: Boolean = true, +// content: @Composable () -> Unit +//) { +// val control = rememberControl { uiNewGrid()!! } +// +//// handleChildren(content) { +//// GridApplier(control.ptr) +//// } +// +// ComposeNode, Applier>>( +// factory = { control.ptr }, +// update = { +// setCommon(enabled, visible) +// set(padded) { uiGridSetPadded(this, if (it) 1 else 0) } +// } +// ) +//} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/MutableListApplier.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/MutableListApplier.kt new file mode 100644 index 00000000..e93f72b2 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/MutableListApplier.kt @@ -0,0 +1,30 @@ +package libui.compose + +import androidx.compose.runtime.AbstractApplier + +internal class MutableListApplier( + private val list: MutableList +) : AbstractApplier(null) { + override fun insertTopDown(index: Int, instance: T?) { + list.add(index, instance!!) + } + + override fun insertBottomUp(index: Int, instance: T?) { + // Ignore, we have plain list + } + + override fun remove(index: Int, count: Int) { + for (i in index + count - 1 downTo index) { + list.removeAt(i) + } + } + + @Suppress("UNCHECKED_CAST") + override fun move(from: Int, to: Int, count: Int) { + (list as MutableList).move(from, to, count) + } + + override fun onClear() { + list.clear() + } +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Tab.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Tab.kt new file mode 100644 index 00000000..aee77769 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Tab.kt @@ -0,0 +1,43 @@ +package libui.compose + +import androidx.compose.runtime.Applier +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import cnames.structs.uiTab +import kotlinx.cinterop.CPointer +import libui.* + + +@Composable +fun TabPane( + enabled: Boolean = true, + visible: Boolean = true, + content: @Composable () -> Unit +) { + val control = rememberControl { uiNewTab()!! } + + handleChildren(content) { TabApplier(control.ptr) } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + } + ) +} + +class TabApplier(private val tab: CPointer) : AppendDeleteApplier() { + override fun appendItem(instance: CPointer?) { + val name = "" + uiTabAppend(tab, name, instance) + } + + override fun deleteItem(index: Int) { + uiTabDelete(tab, index) + } + + override fun insertItemAt(index: Int, instance: CPointer?) { + val name = "" + uiTabInsertAt(tab, name, index, instance) + } +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/ToRemove.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/ToRemove.kt new file mode 100644 index 00000000..8150f748 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/ToRemove.kt @@ -0,0 +1,23 @@ +package libui.compose + +import androidx.compose.runtime.Immutable + +@Immutable +data class SizeInt( + val width: Int, + val height: Int +) + +data class Color( + val r: Double, + val g: Double, + val b: Double, + val a: Double = 1.0 +) { + constructor(rgb: Int, alpha: Double = 1.0) : this( + r = ((rgb shr 16) and 255).toDouble() / 255, + g = ((rgb shr 8) and 255).toDouble() / 255, + b = ((rgb) and 255).toDouble() / 255, + a = alpha + ) +} diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Util.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Util.kt new file mode 100644 index 00000000..c933bed2 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Util.kt @@ -0,0 +1,209 @@ +package libui.compose + +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.Snapshot +import kotlinx.cinterop.* +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import libui.* + +inline fun withLibUI(block: () -> Unit) { + platform.posix.srand(platform.posix.time(null).toUInt()) + + val error = memScoped { + val options = alloc() + uiInit(options.ptr) + } + if (error != null) { + val errorString: String + try { + errorString = error.toKString() + } finally { + uiFreeInitError(error) + } + throw Error("error initializing ui: '$errorString'") + } + + try { + block() + } finally { + // Shutdown libui + uiUninit() + } +} + +internal fun Snapshot.Companion.globalWrites(): Flow { + return callbackFlow { + val handle = registerGlobalWriteObserver { trySend(it) } + awaitClose { handle.dispose() } + } +} + +internal fun CPointer.uiText(): String { + try { + return toKString() + } finally { + uiFreeText(this) + } +} + +@Composable +internal fun rememberControl(block: () -> CPointer): Control { + return remember { Control(block()) } +} + +internal class Control(val ptr: CPointer) : RememberObserver { + override fun onAbandoned() { + uiControlDestroy(ptr.reinterpret()) + } + + override fun onForgotten() { + uiControlDestroy(ptr.reinterpret()) + } + + override fun onRemembered() { + } +} + +@Composable +internal fun handleChildren( + content: @Composable () -> Unit, + applier: () -> Applier<*> +) { + val compContext = rememberCompositionContext() + DisposableEffect(content) { + val composition = Composition(applier(), compContext) + composition.setContent(content) + onDispose { + composition.dispose() + } + } +} + +internal fun Updater>.setCommon(enabled: Boolean, visible: Boolean) { + set(visible) { + if (it) { + uiControlShow(this.reinterpret()) + } else { + uiControlHide(this.reinterpret()) + } + } + set(enabled) { + if (it) { + uiControlEnable(this.reinterpret()) + } else { + uiControlDisable(this.reinterpret()) + } + } +} + +@Composable +internal fun rememberStableRef(data: T): StableRef { + class Wrapper(val ref: StableRef) : RememberObserver { + override fun onAbandoned() { + ref.dispose() + } + + override fun onForgotten() { + ref.dispose() + } + + override fun onRemembered() {} + } + + return remember(data) { Wrapper(StableRef.create(data)) }.ref +} + +abstract class SingletonApplier : AbstractApplier(null) { + protected abstract fun setItem(item: T?) + + override fun insertTopDown(index: Int, instance: T?) { + setItem(instance) + } + + override fun insertBottomUp(index: Int, instance: T?) { + // Ignore, we have a single value + } + + override fun remove(index: Int, count: Int) { + require(index == 0) + require(count <= 1) + if (count > 0) { + setItem(null) + } + } + + override fun move(from: Int, to: Int, count: Int) { + require(count == 0) + } + + override fun onClear() { + setItem(null) + } +} + +abstract class AppendDeleteApplier : Applier?> { + abstract fun deleteItem(index: Int) + abstract fun appendItem(instance: CPointer?) + + open fun insertItemAt(index: Int, instance: CPointer?) { + for (i in controls.lastIndex downTo index) { + deleteItem(i) + } + appendItem(instance) + for (control in controls.drop(index)) { + appendItem(control) + } + } + + + private val controls = mutableListOf>() + private val listApplier = MutableListApplier(controls) + + override fun clear() { + for (i in controls.lastIndex downTo 0) { + deleteItem(i) + } + listApplier.clear() + } + + override fun remove(index: Int, count: Int) { + listApplier.remove(index, count) + repeat(count) { + deleteItem(index) + } + } + + override fun move(from: Int, to: Int, count: Int) { + listApplier.move(from, to, count) + + // TODO: This could be optimised to not remove everything. + + for (i in controls.lastIndex downTo 0) { + deleteItem(i) + } + for (control in controls) { + appendItem(control) + } + } + + override fun insertTopDown(index: Int, instance: CPointer?) { + insertItemAt(index, instance) + listApplier.insertTopDown(index, instance) + } + + override fun insertBottomUp(index: Int, instance: CPointer?) { + listApplier.insertBottomUp(index, instance) + } + + override val current: CPointer? get() = listApplier.current + + override fun up() { + listApplier.up() + } + + override fun down(node: CPointer?) { + listApplier.down(node) + } +} \ No newline at end of file diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Widgets.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Widgets.kt new file mode 100644 index 00000000..710418fd --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Widgets.kt @@ -0,0 +1,364 @@ +@file:Suppress("FunctionName") + +package libui.compose + +import androidx.compose.runtime.* +import cnames.structs.uiButton +import cnames.structs.uiCheckbox +import cnames.structs.uiColorButton +import cnames.structs.uiCombobox +import cnames.structs.uiDateTimePicker +import cnames.structs.uiEditableCombobox +import cnames.structs.uiEntry +import cnames.structs.uiFontButton +import cnames.structs.uiLabel +import cnames.structs.uiMultilineEntry +import cnames.structs.uiProgressBar +import cnames.structs.uiRadioButtons +import cnames.structs.uiSeparator +import cnames.structs.uiSlider +import cnames.structs.uiSpinbox +import kotlinx.cinterop.* +import libui.* +import kotlin.native.concurrent.Worker + +@Composable +fun Label( + text: String, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewLabel(text)!! } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + update(text) { uiLabelSetText(this, it) } + } + ) +} + +// TODO: Only use in HBox +@Composable +fun VerticalSeparator( + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewVerticalSeparator()!! } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + } + ) +} + +// TODO: Only use in VBox +@Composable +fun HorizontalSeparator( + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewHorizontalSeparator()!! } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + } + ) +} + +@Composable +fun ProgressBar( + value: Int = -1, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewProgressBar()!! } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + set(value) { uiProgressBarSetValue(this, it) } + setCommon(enabled, visible) + } + ) +} + +@Composable +fun Button( + text: String, + onClick: () -> Unit, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewButton(text)!! } + + val callback = rememberStableRef(onClick) + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + update(text) { uiButtonSetText(this, it) } + set(callback) { + uiButtonOnClicked( + this, + staticCFunction { _, senderData -> + val ref = senderData!!.asStableRef<() -> Unit>() + ref.get()() + }, + it.asCPointer() + ) + } + } + ) +} + +@Composable +fun ColorButton( + color: MutableState, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewColorButton()!! } + + val state = rememberStableRef(color) + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + set(color.value) { uiColorButtonSetColor(this, it.r, it.g, it.b, it.a) } + set(state) { + uiColorButtonOnChanged( + this, + staticCFunction { ctrl, senderData -> + val ref = senderData!!.asStableRef>() + val array = DoubleArray(4) + array.usePinned { pin -> + uiColorButtonColor( + ctrl, + pin.addressOf(0), + pin.addressOf(1), + pin.addressOf(2), + pin.addressOf(3) + ) + } + ref.get().value = Color(array[0], array[1], array[2], array[3]) + }, + it.asCPointer() + ) + } + } + ) +} + +// FontButton + + + +@Composable +private fun Entry( + control: Control, + text: MutableState, + readOnly: Boolean, + enabled: Boolean, + visible: Boolean, +) { + val state = rememberStableRef(text) + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + set(text.value) { uiEntrySetText(this, it) } + set(readOnly) { uiEntrySetReadOnly(this, if (it) 1 else 0) } + set(state) { + uiEntryOnChanged( + this, + staticCFunction { entry, senderData -> + val ref = senderData!!.asStableRef>() + val data = uiEntryText(entry)!!.uiText() + ref.get().value = data + }, + it.asCPointer() + ) + } + } + ) +} + +@Composable +fun TextField( + text: MutableState, + readOnly: Boolean = false, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewEntry()!! } + Entry(control, text, readOnly, enabled, visible) +} + +@Composable +fun PasswordField( + text: MutableState, + readOnly: Boolean = false, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewPasswordEntry()!! } + Entry(control, text, readOnly, enabled, visible) +} + +@Composable +fun SearchField( + text: MutableState, + readOnly: Boolean = false, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewSearchEntry()!! } + Entry(control, text, readOnly, enabled, visible) +} + + +@Composable +private fun MultilineEntryBase( + control: Control, + text: MutableState, + readOnly: Boolean = true, + enabled: Boolean = true, + visible: Boolean = true, +) { + val state = rememberStableRef(text) + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + set(text.value) { uiMultilineEntrySetText(this, it) } + set(readOnly) { uiMultilineEntrySetReadOnly(this, if (it) 1 else 0) } + set(state) { + uiMultilineEntryOnChanged( + this, + staticCFunction { entry, senderData -> + val ref = senderData!!.asStableRef>() + val data = uiMultilineEntryText(entry)!!.uiText() + ref.get().value = data + }, + it.asCPointer() + ) + } + } + ) +} + +@Composable +fun MultilineEntry( + text: MutableState, + readOnly: Boolean = true, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewMultilineEntry()!! } + MultilineEntryBase(control, text, readOnly, enabled, visible) +} + +@Composable +fun NonWrappingMultilineEntry( + text: MutableState, + readOnly: Boolean = true, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewNonWrappingMultilineEntry()!! } + MultilineEntryBase(control, text, readOnly, enabled, visible) +} + +@Composable +fun Checkbox( + label: String, + checked: MutableState, + enabled: Boolean = true, + visible: Boolean = true, +) { + val control = rememberControl { uiNewCheckbox(label)!! } + + val state = rememberStableRef(checked) + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + update(label) { uiCheckboxSetText(this, it) } + set(checked.value) { uiCheckboxSetChecked(this, if (it) 1 else 0) } + set(state) { + uiCheckboxOnToggled( + this, + staticCFunction { entry, senderData -> + val ref = senderData!!.asStableRef>() + val data = uiCheckboxChecked(entry) != 0 + ref.get().value = data + }, + it.asCPointer() + ) + } + } + ) +} + +// Combobox + +// Spinbox + +@Composable +fun Slider( + value: MutableState, + min: Int, + max: Int, + enabled: Boolean = true, + visible: Boolean = true, +) { + val state = rememberStableRef(value) + + val hack = remember(min, max) { + movableContentOf { + val control = rememberControl { uiNewSlider(min, max)!! } + + ComposeNode, Applier>>( + factory = { control.ptr }, + update = { + setCommon(enabled, visible) + set(value.value) { uiSliderSetValue(this, it) } + set(state) { + uiSliderOnChanged( + this, + staticCFunction { entry, senderData -> + val ref = senderData!!.asStableRef>() + val data = uiSliderValue(entry) + ref.get().value = data + }, + it.asCPointer() + ) + } + update(min) { throw UnsupportedOperationException("Slider.min cannot be changed!") } + update(max) { throw UnsupportedOperationException("Slider.max cannot be changed!") } + } + ) + } + } + + hack() +} + +// uiNewRadioButtons() + +// uiNewDateTimePicker() + +// uiNewDatePicker() + +// uiNewTimePicker() diff --git a/libui-compose/src/nativeMain/kotlin/libui/compose/Window.kt b/libui-compose/src/nativeMain/kotlin/libui/compose/Window.kt new file mode 100644 index 00000000..2c8a6cf5 --- /dev/null +++ b/libui-compose/src/nativeMain/kotlin/libui/compose/Window.kt @@ -0,0 +1,85 @@ +package libui.compose + +import androidx.compose.runtime.* +import cnames.structs.uiWindow +import kotlinx.cinterop.* +import libui.* + +class WindowState(contentSize: SizeInt) { + var contentSize by mutableStateOf(contentSize) +} + +class WindowScope internal constructor() { + @Composable + fun Window( + onCloseRequest: () -> Unit, + state: WindowState, + title: String, + hasMenubar: Boolean = false, + borderless: Boolean = false, + margined: Boolean = false, + fullscreen: Boolean = false, + isVisible: Boolean = true, + enabled: Boolean = true, + content: @Composable () -> Unit, + ) { + val control = rememberControl { + uiNewWindow( + title, + state.contentSize.width, + state.contentSize.height, + if (hasMenubar) 1 else 0 + )!! + } + + handleChildren(content) { + object : SingletonApplier>() { + override fun setItem(item: CPointer?) { + uiWindowSetChild(control.ptr, item) + } + } + } + + val onCloseRef = rememberStableRef(onCloseRequest) + val stateRef = rememberStableRef(state) + + ComposeNode, MutableListApplier>>( + factory = { control.ptr }, + update = { + update(state.contentSize) { (w, h) -> uiWindowSetContentSize(this, w, h) } + update(title) { uiWindowSetTitle(this, it) } + set(borderless) { uiWindowSetBorderless(this, if (it) 1 else 0) } + set(margined) { uiWindowSetMargined(this, if (it) 1 else 0) } + set(fullscreen) { uiWindowSetFullscreen(this, if (it) 1 else 0) } + set(onCloseRef) { + uiWindowOnClosing( + this, + staticCFunction { _, senderData -> + val ref = senderData!!.asStableRef<() -> Unit>() + ref.get()() + 0 + }, + it.asCPointer() + ) + } + set(stateRef) { + uiWindowOnContentSizeChanged( + this, + staticCFunction { sender, senderData -> + val ref = senderData!!.asStableRef() + + val array = IntArray(2) + array.usePinned { pin -> + uiWindowContentSize(sender, pin.addressOf(0), pin.addressOf(1)) + } + + ref.get().contentSize = SizeInt(array[0], array[1]) + }, + it.asCPointer() + ) + } + setCommon(enabled, isVisible) + } + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index fb504968..c400284d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,8 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } } @@ -21,6 +23,7 @@ gradleEnterprise { } include(":libui") +include(":libui-compose") include(":samples:controlgallery") include(":samples:datetime")