From c900ce20c686f9d0daa322f772819c04f206309d Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Tue, 16 Dec 2025 21:13:10 +0100 Subject: [PATCH 1/6] KTNB-1205: Support Jupyter Widgets - platform and 5 kinds of widgets --- gradle/libs.versions.toml | 2 +- .../intellij-platform/build.gradle.kts | 4 - integrations/widgets/build.gradle.kts | 24 +++ .../widgets/widgets-api/build.gradle.kts | 32 +++ .../widget/WidgetJupyterIntegration.kt | 34 ++++ .../kotlinx/jupyter/widget/WidgetManager.kt | 185 ++++++++++++++++++ .../jupyter/widget/library/HtmlWidget.kt | 26 +++ .../jupyter/widget/library/IntSliderWidget.kt | 30 +++ .../jupyter/widget/library/LabelWidget.kt | 25 +++ .../jupyter/widget/library/LayoutWidget.kt | 31 +++ .../jupyter/widget/library/OutputWidget.kt | 25 +++ .../jupyter/widget/model/ModelCreation.kt | 56 ++++++ .../jupyter/widget/model/WidgetModel.kt | 166 ++++++++++++++++ .../jupyter/widget/model/WidgetSpec.kt | 48 +++++ .../types/AbstractWidgetModelPropertyType.kt | 7 + .../model/types/WidgetModelPropertyType.kt | 15 ++ .../widget/model/types/compound/ArrayType.kt | 26 +++ .../model/types/compound/NullableType.kt | 18 ++ .../widget/model/types/datetime/DateType.kt | 32 +++ .../model/types/datetime/DatetimeType.kt | 35 ++++ .../widget/model/types/datetime/TimeType.kt | 32 +++ .../widget/model/types/enums/WidgetEnum.kt | 14 ++ .../model/types/enums/WidgetEnumEntry.kt | 5 + .../model/types/enums/WidgetEnumType.kt | 23 +++ .../model/types/primitive/BooleanType.kt | 3 + .../widget/model/types/primitive/BytesType.kt | 3 + .../widget/model/types/primitive/FloatType.kt | 3 + .../widget/model/types/primitive/IntType.kt | 3 + .../PrimitiveWidgetModelPropertyType.kt | 17 ++ .../model/types/primitive/StringType.kt | 3 + .../model/types/widget/WidgetReferenceType.kt | 39 ++++ .../widget/protocol/BufferPathsSerializer.kt | 68 +++++++ .../jupyter/widget/protocol/Conversion.kt | 3 + .../widget/protocol/JsonSerialization.kt | 76 +++++++ .../jupyter/widget/protocol/PatchHydration.kt | 90 +++++++++ .../jupyter/widget/protocol/WidgetMessage.kt | 72 +++++++ ...kotlinx.jupyter.widget.model.WidgetFactory | 5 + settings.gradle.kts | 3 + 38 files changed, 1278 insertions(+), 5 deletions(-) create mode 100644 integrations/widgets/build.gradle.kts create mode 100644 integrations/widgets/widgets-api/build.gradle.kts create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/AbstractWidgetModelPropertyType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnum.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumEntry.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BooleanType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BytesType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/FloatType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/IntType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/StringType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/BufferPathsSerializer.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/Conversion.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/JsonSerialization.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/PatchHydration.kt create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/WidgetMessage.kt create mode 100644 integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 403a25a..046923d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ # https://intellij-support.jetbrains.com/hc/en-us/articles/206544879-Selecting-the-JDK-version-the-IDE-will-run-under jvmTarget = "17" kotlin = "2.2.20" -kotlin-jupyter = "0.15.1-668" +kotlin-jupyter = "0.15.1-761-1" publishPlugin = "2.2.0-dev-71" intellijPlatformGradlePlugin = "2.10.3" intellijPlatform = "253-EAP-SNAPSHOT" # TODO: lower to 2025.1.3 GA whenever released with required changes diff --git a/integrations/intellij-platform/build.gradle.kts b/integrations/intellij-platform/build.gradle.kts index 856e12b..ef92eee 100644 --- a/integrations/intellij-platform/build.gradle.kts +++ b/integrations/intellij-platform/build.gradle.kts @@ -12,10 +12,6 @@ plugins { val spaceUsername: String by properties val spaceToken: String by properties -allprojects { - version = rootProject.version -} - kotlinJupyter { addApiDependency() } diff --git a/integrations/widgets/build.gradle.kts b/integrations/widgets/build.gradle.kts new file mode 100644 index 0000000..929ff19 --- /dev/null +++ b/integrations/widgets/build.gradle.kts @@ -0,0 +1,24 @@ +import org.jetbrains.kotlinx.publisher.apache2 +import org.jetbrains.kotlinx.publisher.githubRepo + +plugins { + alias(libs.plugins.publisher) +} + +kotlinPublications { + pom { + githubRepo("Kotlin", "kotlin-notebook-integrations") + inceptionYear = "2025" + licenses { + apache2() + } + developers { + developer { + id.set("kotlin-jupyter-team") + name.set("Kotlin Jupyter Team") + organization.set("JetBrains") + organizationUrl.set("https://www.jetbrains.com") + } + } + } +} diff --git a/integrations/widgets/widgets-api/build.gradle.kts b/integrations/widgets/widgets-api/build.gradle.kts new file mode 100644 index 0000000..8fba8f5 --- /dev/null +++ b/integrations/widgets/widgets-api/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.publisher) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.jupyter.api) +} + +dependencies { + compileOnly(libs.kotlinx.serialization.json) + + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlinx.serialization.json) +} + +kotlin { + jvmToolchain( + libs.versions.jvm.toolchain + .get() + .toInt(), + ) + explicitApi() +} + +tasks.processJupyterApiResources { + libraryProducers = listOf("org.jetbrains.kotlinx.jupyter.widget.WidgetJupyterIntegration") +} + +kotlinPublications { + publication { + description.set("Kotlin APIs for IPython Widgets") + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt new file mode 100644 index 0000000..59bc106 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt @@ -0,0 +1,34 @@ +package org.jetbrains.kotlinx.jupyter.widget + +import org.jetbrains.kotlinx.jupyter.api.declare +import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration +import org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel + +private var myWidgetManager: WidgetManager? = null +internal val globalWidgetManager: WidgetManager get() = myWidgetManager!! + +public class WidgetJupyterIntegration : JupyterIntegration() { + override fun Builder.onLoaded() { + importPackage() + importPackage() + + var myLastClassLoader = WidgetJupyterIntegration::class.java.classLoader + val widgetManager = WidgetManager(notebook.commManager) { myLastClassLoader } + myWidgetManager = widgetManager + + onLoaded { + myLastClassLoader = this.lastClassLoader + + declare("widgetManager" to widgetManager) + } + + afterCellExecution { _, _ -> + myLastClassLoader = this.lastClassLoader + } + + renderWithHost { _, widget -> + widgetManager.renderWidget(widget) + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt new file mode 100644 index 0000000..0d147e4 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt @@ -0,0 +1,185 @@ +package org.jetbrains.kotlinx.jupyter.widget + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import org.jetbrains.kotlinx.jupyter.api.DisplayResult +import org.jetbrains.kotlinx.jupyter.api.MimeTypedResultEx +import org.jetbrains.kotlinx.jupyter.api.MimeTypes +import org.jetbrains.kotlinx.jupyter.api.libraries.Comm +import org.jetbrains.kotlinx.jupyter.api.libraries.CommManager +import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MAJOR_VERSION +import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MINOR_VERSION +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.loadWidgetFactory +import org.jetbrains.kotlinx.jupyter.widget.model.versionConstraintRegex +import org.jetbrains.kotlinx.jupyter.widget.protocol.CustomMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStateMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStatesMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.UpdateStatesMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetOpenMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetStateMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetUpdateMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.getWireMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.toPatch + +private val widgetOpenMetadataJson = + buildJsonObject { + put("version", "${DEFAULT_MAJOR_VERSION}.${DEFAULT_MINOR_VERSION}") + } + +public class WidgetManager( + private val commManager: CommManager, + private val classLoaderProvider: () -> ClassLoader, +) { + private val widgetTarget = "jupyter.widget" + private val widgetControlTarget = "jupyter.widget.control" + private val widgets = mutableMapOf() + + init { + commManager.registerCommTarget(widgetControlTarget) { comm, _, _, _ -> + comm.onMessage { msg, _, _ -> + when (Json.decodeFromJsonElement(msg)) { + is RequestStatesMessage -> { + val fullStates = + widgets.mapValues { (id, widget) -> + widget.getFullState() + } + + val wireMessage = getWireMessage(fullStates) + val message = UpdateStatesMessage(wireMessage.state, wireMessage.bufferPaths) + + val data = Json.encodeToJsonElement(message).jsonObject + comm.send(data, null, emptyList()) + } + + else -> {} + } + } + } + + commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers -> + val openMessage = Json.decodeFromJsonElement(data) + val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!! + val widgetFactory = loadWidgetFactory(modelName, classLoaderProvider()) + + val widget = widgetFactory.create() + val patch = openMessage.toPatch(buffers) + widget.applyPatch(patch, this) + + initializeWidget(comm, widget) + } + } + + public fun getWidget(modelId: String): WidgetModel? = widgets[modelId] + + public fun registerWidget(widget: WidgetModel) { + if (widget.id != null) return + + val fullState = widget.getFullState() + val wireMessage = getWireMessage(fullState) + + val comm = + commManager.openComm( + widgetTarget, + Json + .encodeToJsonElement( + WidgetOpenMessage( + wireMessage.state, + wireMessage.bufferPaths, + ), + ).jsonObject, + widgetOpenMetadataJson, + wireMessage.buffers, + ) + + initializeWidget(comm, widget) + } + + public fun renderWidget(widget: WidgetModel): DisplayResult = + MimeTypedResultEx( + buildJsonObject { + val modelId = widget.id ?: error("Widget is not registered") + var versionMajor = DEFAULT_MAJOR_VERSION + var versionMinor = DEFAULT_MINOR_VERSION + var modelName: String? = null + if (widget is DefaultWidgetModel) { + modelName = widget.modelName + val version = widget.modelModuleVersion + val matchResult = versionConstraintRegex.find(version) + if (matchResult != null) { + versionMajor = matchResult.groupValues[1].toInt() + versionMinor = matchResult.groupValues[2].toInt() + } + } + if (modelName != null) { + put(MimeTypes.HTML, "$modelName(id=$modelId)") + } + put( + "application/vnd.jupyter.widget-view+json", + buildJsonObject { + put("version_major", versionMajor) + put("version_minor", versionMinor) + put("model_id", modelId) + }, + ) + }, + null, + ) + + private fun initializeWidget( + comm: Comm, + widget: WidgetModel, + ) { + val modelId = comm.id + widget.setModelId(modelId) + widgets[modelId] = widget + + // Reflect kernel-side changes on the frontend + widget.addChangeListener { patch -> + val wireMessage = getWireMessage(patch) + val data = + Json + .encodeToJsonElement( + WidgetUpdateMessage( + wireMessage.state, + wireMessage.bufferPaths, + ), + ).jsonObject + comm.send(data, null, wireMessage.buffers) + } + + // Reflect frontend-side changes on kernel + comm.onMessage { msg, _, buffers -> + when (val message = Json.decodeFromJsonElement(msg)) { + is WidgetStateMessage -> { + widget.applyPatch(message.toPatch(buffers), this) + } + + is RequestStateMessage -> { + val fullState = widget.getFullState() + val wireMessage = getWireMessage(fullState) + val data = + Json + .encodeToJsonElement( + WidgetUpdateMessage( + wireMessage.state, + wireMessage.bufferPaths, + ), + ).jsonObject + comm.send(data, null, wireMessage.buffers) + } + + is CustomMessage -> {} + + else -> {} + } + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt new file mode 100644 index 0000000..51764da --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt @@ -0,0 +1,26 @@ +package org.jetbrains.kotlinx.jupyter.widget.library + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec +import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec +import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget + +public fun WidgetManager.html(): HtmlWidget = createAndRegisterWidget(HtmlWidget.Factory::class) + +public fun htmlWidget(): HtmlWidget = globalWidgetManager.html() + +public class HtmlWidget internal constructor( + spec: WidgetSpec, +) : DefaultWidgetModel(spec) { + internal class Factory : + DefaultWidgetFactory( + controlsSpec("HTML"), + ::HtmlWidget, + ) + + public var value: String by stringProp("value") + public var layout: LayoutWidget? by widgetProp("layout") +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt new file mode 100644 index 0000000..1b0051b --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt @@ -0,0 +1,30 @@ +package org.jetbrains.kotlinx.jupyter.widget.library + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec +import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec +import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget + +public fun WidgetManager.intSlider(): IntSliderWidget = createAndRegisterWidget(IntSliderWidget.Factory::class) + +public fun intSliderWidget(): IntSliderWidget = globalWidgetManager.intSlider() + +public class IntSliderWidget internal constructor( + spec: WidgetSpec, +) : DefaultWidgetModel(spec) { + internal class Factory : + DefaultWidgetFactory( + controlsSpec("IntSlider"), + ::IntSliderWidget, + ) + + public var value: Int by intProp("value", 0) + public var min: Int by intProp("min", 0) + public var max: Int by intProp("max", 100) + public var step: Int by intProp("step", 1) + public var description: String by stringProp("description", "") + public var layout: LayoutWidget? by widgetProp("layout") +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt new file mode 100644 index 0000000..a13d298 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt @@ -0,0 +1,25 @@ +package org.jetbrains.kotlinx.jupyter.widget.library + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec +import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec +import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget + +public fun WidgetManager.label(): LabelWidget = createAndRegisterWidget(LabelWidget.Factory::class) + +public fun labelWidget(): LabelWidget = globalWidgetManager.label() + +public class LabelWidget internal constructor( + spec: WidgetSpec, +) : DefaultWidgetModel(spec) { + internal class Factory : + DefaultWidgetFactory( + controlsSpec("Label"), + ::LabelWidget, + ) + + public var value: String by stringProp("value", "") +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt new file mode 100644 index 0000000..16e5537 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt @@ -0,0 +1,31 @@ +package org.jetbrains.kotlinx.jupyter.widget.library + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec +import org.jetbrains.kotlinx.jupyter.widget.model.baseSpec +import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget + +public fun WidgetManager.layout(): LayoutWidget = createAndRegisterWidget(LayoutWidget.Factory::class) + +public fun layoutWidget(): LayoutWidget = globalWidgetManager.layout() + +public class LayoutWidget private constructor( + spec: WidgetSpec, +) : DefaultWidgetModel(spec) { + internal class Factory : + DefaultWidgetFactory( + baseSpec("Layout"), + ::LayoutWidget, + ) + + public var layout: String by stringProp("layout", "") + + public var width: String by stringProp("width", "") + public var height: String by stringProp("height", "") + public var display: String by stringProp("display", "") + public var margin: String by stringProp("margin", "") + public var padding: String by stringProp("padding", "") +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt new file mode 100644 index 0000000..2c83417 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt @@ -0,0 +1,25 @@ +package org.jetbrains.kotlinx.jupyter.widget.library + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec +import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec +import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget + +public fun WidgetManager.output(): OutputWidget = createAndRegisterWidget(OutputWidget.Factory::class) + +public fun outputWidget(): OutputWidget = globalWidgetManager.output() + +public class OutputWidget internal constructor( + spec: WidgetSpec, +) : DefaultWidgetModel(spec) { + internal class Factory : + DefaultWidgetFactory( + controlsSpec("Output"), + ::OutputWidget, + ) + + public var layout: LayoutWidget? by widgetProp("layout") +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt new file mode 100644 index 0000000..5d48558 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt @@ -0,0 +1,56 @@ +package org.jetbrains.kotlinx.jupyter.widget.model + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import java.util.ServiceLoader +import java.util.concurrent.ConcurrentHashMap +import kotlin.reflect.KClass + +private val factoryCache = ConcurrentHashMap>() + +internal fun loadWidgetFactory( + modelName: String, + classLoader: ClassLoader, +): WidgetFactory<*> = + factoryCache.getOrPut(modelName) { + ServiceLoader + .load(WidgetFactory::class.java, classLoader) + .firstOrNull { it.spec.modelName == modelName } ?: error("No factory for model $modelName") + } + +public fun WidgetManager.createAndRegisterWidget(widgetFactory: () -> M): M = + widgetFactory().also { widget -> registerWidget(widget) } + +public inline fun , M : WidgetModel> WidgetManager.createAndRegisterWidget(factoryKClass: KClass): M { + @Suppress("UNCHECKED_CAST") + val factory = + factoryKClass.java + .getDeclaredConstructor() + .apply { isAccessible = true } + .newInstance() as WidgetFactory + + return createAndRegisterWidget(factory::create) +} + +public interface WidgetFactory { + public val spec: WidgetSpec + + public fun create(): M +} + +public abstract class DefaultWidgetFactory( + override val spec: WidgetSpec, + private val factory: (spec: WidgetSpec) -> M, +) : WidgetFactory { + override fun create(): M = factory(spec) +} + +public open class DefaultWidgetModel( + spec: WidgetSpec, +) : WidgetModel() { + public val modelName: String by stringProp("_model_name", spec.modelName) + public val modelModule: String by stringProp("_model_module", spec.modelModule) + public val modelModuleVersion: String by stringProp("_model_module_version", spec.modelModuleVersion) + public val viewName: String by stringProp("_view_name", spec.viewName) + public val viewModule: String by stringProp("_view_module", spec.viewModule) + public val viewModuleVersion: String by stringProp("_view_module_version", spec.viewModuleVersion) +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt new file mode 100644 index 0000000..1887a2e --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt @@ -0,0 +1,166 @@ +package org.jetbrains.kotlinx.jupyter.widget.model + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.WidgetModelPropertyType +import org.jetbrains.kotlinx.jupyter.widget.model.types.primitive.BooleanType +import org.jetbrains.kotlinx.jupyter.widget.model.types.primitive.BytesType +import org.jetbrains.kotlinx.jupyter.widget.model.types.primitive.FloatType +import org.jetbrains.kotlinx.jupyter.widget.model.types.primitive.IntType +import org.jetbrains.kotlinx.jupyter.widget.model.types.primitive.StringType +import org.jetbrains.kotlinx.jupyter.widget.model.types.widget.WidgetReferenceType +import org.jetbrains.kotlinx.jupyter.widget.protocol.Patch +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +public abstract class WidgetModel { + private val properties = mutableMapOf>() + private val changeListeners = mutableListOf<(Patch) -> Unit>() + + private var _id: String? = null + public val id: String? get() = _id + + public fun setModelId(modelId: String) { + _id = modelId + } + + public fun getFullState(): Patch = properties.mapValues { (_, property) -> property.serializedValue } + + public fun applyPatch( + patch: Patch, + widgetManager: WidgetManager?, + ) { + for ((key, value) in patch) { + val property = properties[key] ?: continue + property.applyPatch(value, widgetManager) + } + } + + public fun addChangeListener(listener: (Patch) -> Unit) { + changeListeners.add(listener) + } + + private fun addProperty(property: WidgetModelProperty<*>) { + properties[property.name] = property + property.addChangeListener { patch -> + notifyChange(mapOf(property.name to patch)) + } + } + + private fun notifyChange(patch: Patch) { + for (listener in changeListeners) { + listener(patch) + } + } + + protected fun prop( + name: String, + type: WidgetModelPropertyType, + initialValue: T, + ): ReadWriteProperty = WidgetKtPropertyDelegate(name, type, initialValue) + + protected fun stringProp( + name: String, + initialValue: String = "", + ): ReadWriteProperty = prop(name, StringType, initialValue) + + protected fun intProp( + name: String, + initialValue: Int = 0, + ): ReadWriteProperty = prop(name, IntType, initialValue) + + protected fun doubleProp( + name: String, + initialValue: Double = 0.0, + ): ReadWriteProperty = prop(name, FloatType, initialValue) + + protected fun boolProp( + name: String, + initialValue: Boolean = false, + ): ReadWriteProperty = prop(name, BooleanType, initialValue) + + protected fun bytesProp( + name: String, + initialValue: ByteArray = byteArrayOf(), + ): ReadWriteProperty = prop(name, BytesType, initialValue) + + protected fun widgetProp( + name: String, + initialValue: M? = null, + ): ReadWriteProperty = prop(name, WidgetReferenceType(), initialValue) + + protected inner class WidgetKtPropertyDelegate( + private val property: WidgetModelProperty, + ) : ReadWriteProperty { + internal constructor(name: String, type: WidgetModelPropertyType, initialValue: T) : + this(WidgetModelPropertyImpl(name, type, initialValue)) + + init { + addProperty(property) + } + + override fun getValue( + thisRef: WidgetModel, + property: KProperty<*>, + ): T = this.property.value + + override fun setValue( + thisRef: WidgetModel, + property: KProperty<*>, + value: T, + ) { + this.property.value = value + } + } +} + +public interface WidgetModelProperty { + public val name: String + public val type: WidgetModelPropertyType + public var value: T + + public val serializedValue: Any? + + public fun applyPatch( + patch: Any?, + widgetManager: WidgetManager?, + ) + + public fun addChangeListener(listener: (Any?) -> Unit) +} + +internal class WidgetModelPropertyImpl( + override val name: String, + override val type: WidgetModelPropertyType, + initialValue: T, +) : WidgetModelProperty { + private var _value: T = initialValue + private val listeners = mutableListOf<(Any?) -> Unit>() + + override var value: T + get() = _value + set(newValue) { + if (newValue == _value) return + _value = newValue + notifyListeners(newValue) + } + + override val serializedValue: Any? get() = type.serialize(value) + + override fun applyPatch( + patch: Any?, + widgetManager: WidgetManager?, + ) { + value = type.deserialize(patch, widgetManager) + } + + override fun addChangeListener(listener: (Any?) -> Unit) { + listeners.add(listener) + } + + private fun notifyListeners(newValue: T) { + val serializedValue = type.serialize(newValue) + for (listener in listeners) { + listener(serializedValue) + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt new file mode 100644 index 0000000..456834d --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt @@ -0,0 +1,48 @@ +package org.jetbrains.kotlinx.jupyter.widget.model + +import kotlinx.serialization.Serializable + +internal const val DEFAULT_MAJOR_VERSION = 2 +internal const val DEFAULT_MINOR_VERSION = 0 +internal const val DEFAULT_PATCH_VERSION = 0 + +private const val DEFAULT_VERSION_CONSTRAINT = "^$DEFAULT_MAJOR_VERSION.$DEFAULT_MINOR_VERSION.$DEFAULT_PATCH_VERSION" +internal val versionConstraintRegex = Regex("""\D*(\d+)\.(\d+)""") + +/** + * The set of six immutable properties that define a widget specification. + * They should be present in any widget and be distinct across all widgets. + */ +@Serializable +public data class WidgetSpec( + val modelName: String, + val modelModule: String, + val modelModuleVersion: String, + val viewName: String, + val viewModule: String, + val viewModuleVersion: String, +) + +public fun controlsSpec( + controlName: String, + versionConstraint: String = DEFAULT_VERSION_CONSTRAINT, +): WidgetSpec = iPyWidgetsSpec(controlName, "@jupyter-widgets/controls", versionConstraint) + +public fun baseSpec( + controlName: String, + versionConstraint: String = DEFAULT_VERSION_CONSTRAINT, +): WidgetSpec = iPyWidgetsSpec(controlName, "@jupyter-widgets/base", versionConstraint) + +public fun iPyWidgetsSpec( + controlName: String, + moduleName: String, + versionConstraint: String = DEFAULT_VERSION_CONSTRAINT, +): WidgetSpec = + WidgetSpec( + modelName = controlName + "Model", + modelModule = moduleName, + modelModuleVersion = versionConstraint, + viewName = controlName + "View", + viewModule = moduleName, + viewModuleVersion = versionConstraint, + ) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/AbstractWidgetModelPropertyType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/AbstractWidgetModelPropertyType.kt new file mode 100644 index 0000000..0531096 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/AbstractWidgetModelPropertyType.kt @@ -0,0 +1,7 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types + +public abstract class AbstractWidgetModelPropertyType( + override val name: String, +) : WidgetModelPropertyType { + override fun toString(): String = name +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt new file mode 100644 index 0000000..78fa35f --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt @@ -0,0 +1,15 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager + +public interface WidgetModelPropertyType { + public val name: String + public val default: T + + public fun serialize(propertyValue: T): Any? + + public fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): T +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt new file mode 100644 index 0000000..4c49d22 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt @@ -0,0 +1,26 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.compound + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType +import org.jetbrains.kotlinx.jupyter.widget.model.types.WidgetModelPropertyType + +public class ArrayType( + public val elementType: WidgetModelPropertyType, +) : AbstractWidgetModelPropertyType>("array<${elementType.name}>") { + override val default: List = emptyList() + + override fun serialize(propertyValue: List): List = propertyValue.map { elementType.serialize(it) } + + @Suppress("UNCHECKED_CAST") + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): List { + require(patchValue is List<*>) { + "Expected List for $name, got ${patchValue?.let { it::class.simpleName } ?: "null"}" + } + return patchValue.map { raw -> + elementType.deserialize(raw, widgetManager) + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt new file mode 100644 index 0000000..55ec7a5 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt @@ -0,0 +1,18 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.compound + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType +import org.jetbrains.kotlinx.jupyter.widget.model.types.WidgetModelPropertyType + +public class NullableType( + private val inner: WidgetModelPropertyType, +) : AbstractWidgetModelPropertyType("${inner.name}?") { + override val default: T? = null + + override fun serialize(propertyValue: T?): Any? = propertyValue?.let { inner.serialize(it) } + + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): T? = if (patchValue == null) null else inner.deserialize(patchValue, widgetManager) +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt new file mode 100644 index 0000000..88b2017 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt @@ -0,0 +1,32 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.datetime + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.ResolverStyle + +public object DateType : AbstractWidgetModelPropertyType("date") { + override val default: LocalDate = LocalDate.EPOCH + + private val formatter: DateTimeFormatter = + DateTimeFormatter + .ofPattern("uuuu-MM-dd") + .withResolverStyle(ResolverStyle.STRICT) + + override fun serialize(propertyValue: LocalDate): Any? = propertyValue.format(formatter) + + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): LocalDate { + require(patchValue is String) { + "Expected String for date, got ${patchValue?.let { it::class.simpleName } ?: "null"}" + } + return try { + LocalDate.parse(patchValue, formatter) + } catch (e: Exception) { + error("Invalid date format '$patchValue', expected uuuu-MM-dd: ${e.message}") + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt new file mode 100644 index 0000000..91a25e1 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt @@ -0,0 +1,35 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.datetime + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.ResolverStyle + +public object DatetimeType : AbstractWidgetModelPropertyType("datetime") { + override val default: Instant = Instant.EPOCH + + private val formatter: DateTimeFormatter = + DateTimeFormatter + .ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'") + .withResolverStyle(ResolverStyle.STRICT) + .withZone(ZoneOffset.UTC) + + override fun serialize(propertyValue: Instant): Any? = formatter.format(propertyValue) // respect 'Z' + + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): Instant { + require(patchValue is String) { + "Expected String for datetime, got ${patchValue?.let { it::class.simpleName } ?: "null"}" + } + return try { + ZonedDateTime.parse(patchValue, formatter).toInstant() + } catch (e: Exception) { + error("Invalid datetime format '$patchValue', expected uuuu-MM-dd'T'HH:mm:ss'Z': ${e.message}") + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt new file mode 100644 index 0000000..193b1ba --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt @@ -0,0 +1,32 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.datetime + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.time.format.ResolverStyle + +public object TimeType : AbstractWidgetModelPropertyType("time") { + override val default: LocalTime = LocalTime.MIDNIGHT + + private val formatter: DateTimeFormatter = + DateTimeFormatter + .ofPattern("HH:mm:ss") + .withResolverStyle(ResolverStyle.STRICT) + + override fun serialize(propertyValue: LocalTime): Any? = propertyValue.format(formatter) + + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): LocalTime { + require(patchValue is String) { + "Expected String for time, got ${patchValue?.let { it::class.simpleName } ?: "null"}" + } + return try { + LocalTime.parse(patchValue, formatter) + } catch (e: Exception) { + error("Invalid time format '$patchValue', expected HH:mm:ss: ${e.message}") + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnum.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnum.kt new file mode 100644 index 0000000..5fa10db --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnum.kt @@ -0,0 +1,14 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.enums + +import kotlin.properties.ReadOnlyProperty + +public abstract class WidgetEnum> { + private val _entries = mutableListOf>() + public val entries: List> get() = _entries + + protected fun entry(name: String): ReadOnlyProperty, WidgetEnumEntry> { + val e = WidgetEnumEntry(name) + _entries.add(e) + return ReadOnlyProperty { _, _ -> e } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumEntry.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumEntry.kt new file mode 100644 index 0000000..176d448 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumEntry.kt @@ -0,0 +1,5 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.enums + +public class WidgetEnumEntry>( + public val name: String, +) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt new file mode 100644 index 0000000..5bfb934 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt @@ -0,0 +1,23 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.enums + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType + +public class WidgetEnumType>( + private val widgetEnum: E, + override val default: WidgetEnumEntry, +) : AbstractWidgetModelPropertyType>("enum") { + override fun serialize(propertyValue: WidgetEnumEntry): String = propertyValue.name + + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): WidgetEnumEntry { + require(patchValue is String) { + "Expected String for enum, got ${patchValue?.let { it::class.simpleName } ?: "null"}" + } + return widgetEnum.entries.first { + it.name == patchValue + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BooleanType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BooleanType.kt new file mode 100644 index 0000000..a10fc20 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BooleanType.kt @@ -0,0 +1,3 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.primitive + +public object BooleanType : PrimitiveWidgetModelPropertyType("boolean", false) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BytesType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BytesType.kt new file mode 100644 index 0000000..b5ddbda --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/BytesType.kt @@ -0,0 +1,3 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.primitive + +public object BytesType : PrimitiveWidgetModelPropertyType("bytes", byteArrayOf()) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/FloatType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/FloatType.kt new file mode 100644 index 0000000..507ccf4 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/FloatType.kt @@ -0,0 +1,3 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.primitive + +public object FloatType : PrimitiveWidgetModelPropertyType("float", 0.0) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/IntType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/IntType.kt new file mode 100644 index 0000000..52de466 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/IntType.kt @@ -0,0 +1,3 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.primitive + +public object IntType : PrimitiveWidgetModelPropertyType("int", 0) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt new file mode 100644 index 0000000..376d8b4 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.primitive + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType + +public abstract class PrimitiveWidgetModelPropertyType( + name: String, + override val default: T, +) : AbstractWidgetModelPropertyType(name) { + override fun serialize(propertyValue: T): Any? = propertyValue + + @Suppress("UNCHECKED_CAST") + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): T = patchValue as T +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/StringType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/StringType.kt new file mode 100644 index 0000000..b77897a --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/StringType.kt @@ -0,0 +1,3 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.primitive + +public object StringType : PrimitiveWidgetModelPropertyType("string", "") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt new file mode 100644 index 0000000..b91b3b1 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt @@ -0,0 +1,39 @@ +package org.jetbrains.kotlinx.jupyter.widget.model.types.widget + +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.types.AbstractWidgetModelPropertyType + +private const val WIDGET_REF_PREFIX = "IPY_MODEL_" + +public class WidgetReferenceType : AbstractWidgetModelPropertyType("widget-ref") { + override val default: M? get() = null + + override fun serialize(propertyValue: M?): String? = + propertyValue?.let { + "$WIDGET_REF_PREFIX${it.id}" + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize( + patchValue: Any?, + widgetManager: WidgetManager?, + ): M? { + if (patchValue == null) return null + + requireNotNull(widgetManager) { + "Widget manager is required to deserialize widget-ref" + } + require(patchValue is String) { + "Expected String for widget-ref, got ${patchValue::class.simpleName}" + } + require(patchValue.startsWith(WIDGET_REF_PREFIX)) { + "Invalid widget ref format: $patchValue" + } + val id = patchValue.removePrefix(WIDGET_REF_PREFIX) + val model = + widgetManager.getWidget(id) + ?: error("Widget with id=$id not found") + return model as M + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/BufferPathsSerializer.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/BufferPathsSerializer.kt new file mode 100644 index 0000000..d90da34 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/BufferPathsSerializer.kt @@ -0,0 +1,68 @@ +package org.jetbrains.kotlinx.jupyter.widget.protocol + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.listSerialDescriptor +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.intOrNull + +internal object BufferPathsSerializer : KSerializer>> { + @OptIn(ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor = + listSerialDescriptor( + listSerialDescriptor( + serialDescriptor(), + ), + ) + + override fun serialize( + encoder: Encoder, + value: List>, + ) { + val jsonEncoder = + encoder as? JsonEncoder + ?: error("BufferPathsSerializer can only be used with JSON") + jsonEncoder.encodeJsonElement( + JsonArray( + value.map { path -> + JsonArray( + path.map { el -> + when (el) { + is String -> JsonPrimitive(el) + is Number -> JsonPrimitive(el) + else -> error("Unsupported buffer path element: $el (${el::class.simpleName})") + } + }, + ) + }, + ), + ) + } + + override fun deserialize(decoder: Decoder): List> { + val jsonDecoder = + decoder as? JsonDecoder + ?: error("BufferPathsSerializer can only be used with JSON") + val element = jsonDecoder.decodeJsonElement() + + require(element is JsonArray) { "Expected JSON array for buffer_paths, got: $element" } + + return element.map { pathEl -> + require(pathEl is JsonArray) { "Expected JSON array inside buffer_paths, got: $pathEl" } + pathEl.map { element -> + element as? JsonPrimitive ?: error("Expected JSON primitive inside buffer_paths, got: $element") + if (element.isString) return@map element.content + element.doubleOrNull ?: element.intOrNull ?: error("Unsupported buffer path element: $element") + } + } + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/Conversion.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/Conversion.kt new file mode 100644 index 0000000..eb9aaa5 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/Conversion.kt @@ -0,0 +1,3 @@ +package org.jetbrains.kotlinx.jupyter.widget.protocol + +internal fun WidgetStateMessage.toPatch(buffers: List): Patch = getPatch(WireMessage(state, bufferPaths, buffers)) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/JsonSerialization.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/JsonSerialization.kt new file mode 100644 index 0000000..1fe6e47 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/JsonSerialization.kt @@ -0,0 +1,76 @@ +package org.jetbrains.kotlinx.jupyter.widget.protocol + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.doubleOrNull +import kotlin.collections.iterator + +internal fun serializeJsonMap(map: Patch): JsonElement = serialize(map) + +private fun serializeAny(obj: Any?): JsonElement = + when (obj) { + null -> JsonNull + is Map<*, *> -> serialize(obj) + is List<*> -> serialize(obj) + is String -> JsonPrimitive(obj) + is Boolean -> JsonPrimitive(obj) + is Number -> JsonPrimitive(obj) + is ByteArray -> JsonNull + else -> error("Don't know how to serialize object [$obj] of class ${obj::class}") + } + +private fun serialize(map: Map<*, *>): JsonObject = + buildJsonObject { + for ((key, value) in map) { + if (key !is String) error("Map key [$key] is of type ${key?.let { it::class }}. Don't know how to serialize it.") + put(key, serializeAny(value)) + } + } + +private fun serialize(list: List<*>): JsonArray = + buildJsonArray { + for (value in list) { + add(serializeAny(value)) + } + } + +internal fun deserializeJsonMap(json: JsonElement): MutableMap { + if (json !is JsonObject) error("Input json should be a key-value object, but it's $json") + return deserializeMap(json) +} + +private fun deserializeAny(json: JsonElement): Any? = + when (json) { + is JsonObject -> deserializeMap(json) + is JsonArray -> deserializeList(json) + is JsonPrimitive -> deserializePrimitive(json) + } + +private fun deserializePrimitive(json: JsonPrimitive): Any? = + when { + json is JsonNull -> null + json.isString -> json.content + else -> { + json.booleanOrNull ?: json.doubleOrNull ?: error("Unknown JSON primitive type: [$json]") + } + } + +private fun deserializeMap(json: JsonObject): MutableMap = + mutableMapOf().apply { + for ((key, value) in json) { + put(key, deserializeAny(value)) + } + } + +private fun deserializeList(jsonArray: JsonArray): MutableList = + mutableListOf().apply { + for (el in jsonArray) { + add(deserializeAny(el)) + } + } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/PatchHydration.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/PatchHydration.kt new file mode 100644 index 0000000..2d29cdd --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/PatchHydration.kt @@ -0,0 +1,90 @@ +package org.jetbrains.kotlinx.jupyter.widget.protocol + +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlin.collections.get +import kotlin.collections.iterator + +/** + * Patch with inserted byte buffers - we call it "hydrated patch" or just "patch" + * for the sake of simplicity + */ +public typealias Patch = Map + +internal data class WireMessage( + val state: JsonObject, + val bufferPaths: List>, + val buffers: List, +) + +internal fun getPatch(wireMessage: WireMessage): Patch { + val dehydratedMap = deserializeJsonMap(wireMessage.state) + for ((path, buf) in wireMessage.bufferPaths.zip(wireMessage.buffers)) { + var obj: Any? = dehydratedMap + for (key in path.dropLast(1)) obj = getAt(obj, key) + setAt(obj, path.last(), buf) + } + return dehydratedMap +} + +internal fun getWireMessage(patch: Patch): WireMessage { + val pathStack = mutableListOf() + val bufferPaths = mutableListOf>() + val buffers = mutableListOf() + + fun traverse(obj: Any?) { + when (obj) { + is Map<*, *> -> { + for ((key, value) in obj) { + pathStack.add(key as Any) + traverse(value) + pathStack.removeLast() + } + } + is List<*> -> { + for (i in obj.indices) { + pathStack.add(i) + traverse(obj[i]) + pathStack.removeLast() + } + } + is ByteArray -> { + bufferPaths.add(pathStack.toList()) + buffers.add(obj) + } + } + } + + traverse(patch) + val state = serializeJsonMap(patch).jsonObject + return WireMessage(state, bufferPaths, buffers) +} + +private fun getAt( + obj: Any?, + key: Any, +): Any? = + when (obj) { + is Map<*, *> -> obj[key as String] + is List<*> -> obj[key as Int] + else -> error("Unexpected object type: $obj") + } + +private fun setAt( + obj: Any?, + key: Any?, + value: Any?, +) { + when (obj) { + is MutableMap<*, *> -> { + @Suppress("UNCHECKED_CAST") + (obj as MutableMap)[key] = value + } + + is MutableList<*> -> { + @Suppress("UNCHECKED_CAST") + (obj as MutableList)[key as Int] = value + } + else -> error("Unexpected object type: $obj") + } +} diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/WidgetMessage.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/WidgetMessage.kt new file mode 100644 index 0000000..34303d0 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/protocol/WidgetMessage.kt @@ -0,0 +1,72 @@ +package org.jetbrains.kotlinx.jupyter.widget.protocol + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator +import kotlinx.serialization.json.JsonObject + +@OptIn(ExperimentalSerializationApi::class) +@JsonClassDiscriminator("method") +@Serializable +internal sealed interface WidgetMessage + +internal interface WithBufferPaths { + val bufferPaths: List> +} + +internal interface WidgetStateMessage : WithBufferPaths { + val state: JsonObject +} + +@Serializable +internal class WidgetOpenMessage( + override val state: JsonObject, + @SerialName("buffer_paths") + @Serializable(with = BufferPathsSerializer::class) + override val bufferPaths: List>, +) : WidgetStateMessage + +@Serializable +@SerialName("update") +internal class WidgetUpdateMessage( + override val state: JsonObject, + @SerialName("buffer_paths") + @Serializable(with = BufferPathsSerializer::class) + override val bufferPaths: List>, +) : WidgetMessage, + WidgetStateMessage + +@Serializable +@SerialName("echo_update") +internal class WidgetEchoUpdateMessage( + override val state: JsonObject, + @SerialName("buffer_paths") + @Serializable(with = BufferPathsSerializer::class) + override val bufferPaths: List>, +) : WidgetMessage, + WidgetStateMessage + +@Serializable +@SerialName("request_state") +internal class RequestStateMessage : WidgetMessage + +@Serializable +@SerialName("custom") +internal class CustomMessage( + val content: JsonObject, +) : WidgetMessage + +@Serializable +@SerialName("request_states") +internal class RequestStatesMessage : WidgetMessage + +@Serializable +@SerialName("update_states") +internal class UpdateStatesMessage( + val states: JsonObject, + @SerialName("buffer_paths") + @Serializable(with = BufferPathsSerializer::class) + override val bufferPaths: List>, +) : WidgetMessage, + WithBufferPaths diff --git a/integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory b/integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory new file mode 100644 index 0000000..8a92bc4 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory @@ -0,0 +1,5 @@ +org.jetbrains.kotlinx.jupyter.widget.library.HtmlWidget$Factory +org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget$Factory +org.jetbrains.kotlinx.jupyter.widget.library.LabelWidget$Factory +org.jetbrains.kotlinx.jupyter.widget.library.LayoutWidget$Factory +org.jetbrains.kotlinx.jupyter.widget.library.OutputWidget$Factory diff --git a/settings.gradle.kts b/settings.gradle.kts index c65133b..5826c58 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,9 @@ projectStructure { project("database-integration-tests") } project("intellij-platform") + folder("widgets") { + project("widgets-api") + } } } From 72ede39f838f1f6c403c0667e8ecb6dd7fef49e9 Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Wed, 17 Dec 2025 02:14:45 +0100 Subject: [PATCH 2/6] KTNB-1205: Extract widget factory management into `WidgetFactoryRegistry` for improved modularity. --- .../kotlinx/jupyter/widget/WidgetManager.kt | 4 ++-- .../jupyter/widget/model/ModelCreation.kt | 14 ------------ .../widget/model/WidgetFactoryRegistry.kt | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt index 0d147e4..1651f50 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt @@ -15,8 +15,8 @@ import org.jetbrains.kotlinx.jupyter.api.libraries.CommManager import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MAJOR_VERSION import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MINOR_VERSION import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactoryRegistry import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.loadWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.versionConstraintRegex import org.jetbrains.kotlinx.jupyter.widget.protocol.CustomMessage import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStateMessage @@ -67,7 +67,7 @@ public class WidgetManager( commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers -> val openMessage = Json.decodeFromJsonElement(data) val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!! - val widgetFactory = loadWidgetFactory(modelName, classLoaderProvider()) + val widgetFactory = WidgetFactoryRegistry.loadWidgetFactory(modelName, classLoaderProvider()) val widget = widgetFactory.create() val patch = openMessage.toPatch(buffers) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt index 5d48558..548eeb9 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt @@ -1,22 +1,8 @@ package org.jetbrains.kotlinx.jupyter.widget.model import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import java.util.ServiceLoader -import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass -private val factoryCache = ConcurrentHashMap>() - -internal fun loadWidgetFactory( - modelName: String, - classLoader: ClassLoader, -): WidgetFactory<*> = - factoryCache.getOrPut(modelName) { - ServiceLoader - .load(WidgetFactory::class.java, classLoader) - .firstOrNull { it.spec.modelName == modelName } ?: error("No factory for model $modelName") - } - public fun WidgetManager.createAndRegisterWidget(widgetFactory: () -> M): M = widgetFactory().also { widget -> registerWidget(widget) } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt new file mode 100644 index 0000000..301e571 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlinx.jupyter.widget.model + +import java.util.ServiceLoader +import java.util.concurrent.ConcurrentHashMap + +public object WidgetFactoryRegistry { + private val factoryCache = ConcurrentHashMap>() + + internal fun loadWidgetFactory( + modelName: String, + classLoader: ClassLoader, + ): WidgetFactory<*> = + factoryCache.getOrPut(modelName) { + ServiceLoader + .load(WidgetFactory::class.java, classLoader) + .firstOrNull { it.spec.modelName == modelName } ?: error("No factory for model $modelName") + } + + public fun registerWidgetFactory(factory: WidgetFactory<*>) { + factoryCache[factory.spec.modelName] = factory + } +} From fdaa6757b83e70bb39aa73c229bfcf45d1f7360b Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Thu, 18 Dec 2025 01:30:47 +0100 Subject: [PATCH 3/6] KTNB-1205: Refactor widget initialization to use WidgetFactoryRegistry and simplify factory creation. --- .../widgets/widgets-api/build.gradle.kts | 1 + .../kotlinx/jupyter/widget/WidgetManager.kt | 6 +++-- .../jupyter/widget/library/HtmlWidget.kt | 15 ++++------- .../jupyter/widget/library/IntSliderWidget.kt | 15 ++++------- .../jupyter/widget/library/LabelWidget.kt | 15 ++++------- .../jupyter/widget/library/LayoutWidget.kt | 15 ++++------- .../jupyter/widget/library/OutputWidget.kt | 17 ++++++------- .../registry/DefaultWidgetFactories.kt | 17 +++++++++++++ .../jupyter/widget/model/ModelCreation.kt | 25 +++++++------------ .../widget/model/WidgetFactoryRegistry.kt | 9 ++++++- .../jupyter/widget/model/WidgetSpec.kt | 5 ++++ ...kotlinx.jupyter.widget.model.WidgetFactory | 5 ---- 12 files changed, 71 insertions(+), 74 deletions(-) create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/registry/DefaultWidgetFactories.kt delete mode 100644 integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory diff --git a/integrations/widgets/widgets-api/build.gradle.kts b/integrations/widgets/widgets-api/build.gradle.kts index 8fba8f5..a4ebbe0 100644 --- a/integrations/widgets/widgets-api/build.gradle.kts +++ b/integrations/widgets/widgets-api/build.gradle.kts @@ -7,6 +7,7 @@ plugins { dependencies { compileOnly(libs.kotlinx.serialization.json) + implementation(libs.kotlin.reflect) testImplementation(libs.kotlin.test) testImplementation(libs.kotlinx.serialization.json) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt index 1651f50..959522c 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt @@ -42,6 +42,8 @@ public class WidgetManager( private val widgetControlTarget = "jupyter.widget.control" private val widgets = mutableMapOf() + public val factoryRegistry: WidgetFactoryRegistry = WidgetFactoryRegistry() + init { commManager.registerCommTarget(widgetControlTarget) { comm, _, _, _ -> comm.onMessage { msg, _, _ -> @@ -67,9 +69,9 @@ public class WidgetManager( commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers -> val openMessage = Json.decodeFromJsonElement(data) val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!! - val widgetFactory = WidgetFactoryRegistry.loadWidgetFactory(modelName, classLoaderProvider()) + val widgetFactory = factoryRegistry.loadWidgetFactory(modelName, classLoaderProvider()) - val widget = widgetFactory.create() + val widget = widgetFactory.create(this) val patch = openMessage.toPatch(buffers) widget.applyPatch(patch, this) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt index 51764da..b0bf417 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt @@ -4,22 +4,17 @@ import org.jetbrains.kotlinx.jupyter.widget.WidgetManager import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget -public fun WidgetManager.html(): HtmlWidget = createAndRegisterWidget(HtmlWidget.Factory::class) +public fun WidgetManager.html(): HtmlWidget = createAndRegisterWidget(HtmlWidget.Factory) public fun htmlWidget(): HtmlWidget = globalWidgetManager.html() -public class HtmlWidget internal constructor( - spec: WidgetSpec, -) : DefaultWidgetModel(spec) { - internal class Factory : - DefaultWidgetFactory( - controlsSpec("HTML"), - ::HtmlWidget, - ) +private val spec = controlsSpec("HTML") + +public class HtmlWidget internal constructor() : DefaultWidgetModel(spec) { + internal object Factory : DefaultWidgetFactory(spec, ::HtmlWidget) public var value: String by stringProp("value") public var layout: LayoutWidget? by widgetProp("layout") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt index 1b0051b..aa46de4 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt @@ -4,22 +4,17 @@ import org.jetbrains.kotlinx.jupyter.widget.WidgetManager import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget -public fun WidgetManager.intSlider(): IntSliderWidget = createAndRegisterWidget(IntSliderWidget.Factory::class) +public fun WidgetManager.intSlider(): IntSliderWidget = createAndRegisterWidget(IntSliderWidget.Factory) public fun intSliderWidget(): IntSliderWidget = globalWidgetManager.intSlider() -public class IntSliderWidget internal constructor( - spec: WidgetSpec, -) : DefaultWidgetModel(spec) { - internal class Factory : - DefaultWidgetFactory( - controlsSpec("IntSlider"), - ::IntSliderWidget, - ) +private val spec = controlsSpec("IntSlider") + +public class IntSliderWidget internal constructor() : DefaultWidgetModel(spec) { + internal object Factory : DefaultWidgetFactory(spec, ::IntSliderWidget) public var value: Int by intProp("value", 0) public var min: Int by intProp("min", 0) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt index a13d298..94ca5c2 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt @@ -4,22 +4,17 @@ import org.jetbrains.kotlinx.jupyter.widget.WidgetManager import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget -public fun WidgetManager.label(): LabelWidget = createAndRegisterWidget(LabelWidget.Factory::class) +public fun WidgetManager.label(): LabelWidget = createAndRegisterWidget(LabelWidget.Factory) public fun labelWidget(): LabelWidget = globalWidgetManager.label() -public class LabelWidget internal constructor( - spec: WidgetSpec, -) : DefaultWidgetModel(spec) { - internal class Factory : - DefaultWidgetFactory( - controlsSpec("Label"), - ::LabelWidget, - ) +private val spec = controlsSpec("Label") + +public class LabelWidget internal constructor() : DefaultWidgetModel(spec) { + internal object Factory : DefaultWidgetFactory(spec, ::LabelWidget) public var value: String by stringProp("value", "") } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt index 16e5537..8d81697 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt @@ -4,22 +4,17 @@ import org.jetbrains.kotlinx.jupyter.widget.WidgetManager import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec import org.jetbrains.kotlinx.jupyter.widget.model.baseSpec import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget -public fun WidgetManager.layout(): LayoutWidget = createAndRegisterWidget(LayoutWidget.Factory::class) +public fun WidgetManager.layout(): LayoutWidget = createAndRegisterWidget(LayoutWidget.Factory) public fun layoutWidget(): LayoutWidget = globalWidgetManager.layout() -public class LayoutWidget private constructor( - spec: WidgetSpec, -) : DefaultWidgetModel(spec) { - internal class Factory : - DefaultWidgetFactory( - baseSpec("Layout"), - ::LayoutWidget, - ) +private val spec = baseSpec("Layout") + +public class LayoutWidget private constructor() : DefaultWidgetModel(spec) { + internal object Factory : DefaultWidgetFactory(spec, ::LayoutWidget) public var layout: String by stringProp("layout", "") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt index 2c83417..b417a47 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt @@ -4,22 +4,19 @@ import org.jetbrains.kotlinx.jupyter.widget.WidgetManager import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.WidgetSpec -import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget +import org.jetbrains.kotlinx.jupyter.widget.model.outputSpec -public fun WidgetManager.output(): OutputWidget = createAndRegisterWidget(OutputWidget.Factory::class) +public fun WidgetManager.output(): OutputWidget = createAndRegisterWidget(OutputWidget.Factory) public fun outputWidget(): OutputWidget = globalWidgetManager.output() +private val spec = outputSpec("Output") + public class OutputWidget internal constructor( - spec: WidgetSpec, + widgetManager: WidgetManager, ) : DefaultWidgetModel(spec) { - internal class Factory : - DefaultWidgetFactory( - controlsSpec("Output"), - ::OutputWidget, - ) + internal object Factory : DefaultWidgetFactory(spec, ::OutputWidget) - public var layout: LayoutWidget? by widgetProp("layout") + public var layout: LayoutWidget? by widgetProp("layout", widgetManager.layout()) } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/registry/DefaultWidgetFactories.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/registry/DefaultWidgetFactories.kt new file mode 100644 index 0000000..43917a2 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/registry/DefaultWidgetFactories.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlinx.jupyter.widget.library.registry + +import org.jetbrains.kotlinx.jupyter.widget.library.HtmlWidget +import org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget +import org.jetbrains.kotlinx.jupyter.widget.library.LabelWidget +import org.jetbrains.kotlinx.jupyter.widget.library.LayoutWidget +import org.jetbrains.kotlinx.jupyter.widget.library.OutputWidget +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory + +internal val defaultWidgetFactories = + listOf>( + HtmlWidget.Factory, + IntSliderWidget.Factory, + LabelWidget.Factory, + LayoutWidget.Factory, + OutputWidget.Factory, + ) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt index 548eeb9..e95deb3 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt @@ -1,33 +1,26 @@ package org.jetbrains.kotlinx.jupyter.widget.model import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import kotlin.reflect.KClass -public fun WidgetManager.createAndRegisterWidget(widgetFactory: () -> M): M = - widgetFactory().also { widget -> registerWidget(widget) } +public fun WidgetManager.createAndRegisterWidget(widgetFactory: (widgetManager: WidgetManager) -> M): M = + widgetFactory(this).also { widget -> registerWidget(widget) } -public inline fun , M : WidgetModel> WidgetManager.createAndRegisterWidget(factoryKClass: KClass): M { - @Suppress("UNCHECKED_CAST") - val factory = - factoryKClass.java - .getDeclaredConstructor() - .apply { isAccessible = true } - .newInstance() as WidgetFactory - - return createAndRegisterWidget(factory::create) -} +public fun WidgetManager.createAndRegisterWidget(factory: WidgetFactory): M = createAndRegisterWidget(factory::create) public interface WidgetFactory { public val spec: WidgetSpec - public fun create(): M + public fun create(widgetManager: WidgetManager): M } public abstract class DefaultWidgetFactory( override val spec: WidgetSpec, - private val factory: (spec: WidgetSpec) -> M, + private val factory: (widgetManager: WidgetManager) -> M, ) : WidgetFactory { - override fun create(): M = factory(spec) + public constructor(spec: WidgetSpec, factory: () -> M) : + this(spec, { _ -> factory() }) + + override fun create(widgetManager: WidgetManager): M = factory(widgetManager) } public open class DefaultWidgetModel( diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt index 301e571..c416e25 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetFactoryRegistry.kt @@ -1,11 +1,18 @@ package org.jetbrains.kotlinx.jupyter.widget.model +import org.jetbrains.kotlinx.jupyter.widget.library.registry.defaultWidgetFactories import java.util.ServiceLoader import java.util.concurrent.ConcurrentHashMap -public object WidgetFactoryRegistry { +public class WidgetFactoryRegistry { private val factoryCache = ConcurrentHashMap>() + init { + for (factory in defaultWidgetFactories) { + registerWidgetFactory(factory) + } + } + internal fun loadWidgetFactory( modelName: String, classLoader: ClassLoader, diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt index 456834d..37d7d8a 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetSpec.kt @@ -33,6 +33,11 @@ public fun baseSpec( versionConstraint: String = DEFAULT_VERSION_CONSTRAINT, ): WidgetSpec = iPyWidgetsSpec(controlName, "@jupyter-widgets/base", versionConstraint) +public fun outputSpec( + controlName: String, + versionConstraint: String = DEFAULT_VERSION_CONSTRAINT, +): WidgetSpec = iPyWidgetsSpec(controlName, "@jupyter-widgets/output", versionConstraint) + public fun iPyWidgetsSpec( controlName: String, moduleName: String, diff --git a/integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory b/integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory deleted file mode 100644 index 8a92bc4..0000000 --- a/integrations/widgets/widgets-api/src/main/resources/META-INF/services/org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactory +++ /dev/null @@ -1,5 +0,0 @@ -org.jetbrains.kotlinx.jupyter.widget.library.HtmlWidget$Factory -org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget$Factory -org.jetbrains.kotlinx.jupyter.widget.library.LabelWidget$Factory -org.jetbrains.kotlinx.jupyter.widget.library.LayoutWidget$Factory -org.jetbrains.kotlinx.jupyter.widget.library.OutputWidget$Factory From 5b2e3f949bd6200b0313ce7e7bc2501da644b68a Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Fri, 19 Dec 2025 02:17:18 +0100 Subject: [PATCH 4/6] KTNB-1205: Pass `WidgetManager` as a constructor parameter to WidgetModel, get rid of ugly ID registration --- .../kotlinx/jupyter/widget/WidgetManager.kt | 13 ++++--- .../jupyter/widget/library/HtmlWidget.kt | 4 ++- .../jupyter/widget/library/IntSliderWidget.kt | 4 ++- .../jupyter/widget/library/LabelWidget.kt | 4 ++- .../jupyter/widget/library/LayoutWidget.kt | 4 ++- .../jupyter/widget/library/OutputWidget.kt | 2 +- .../jupyter/widget/model/ModelCreation.kt | 3 +- .../jupyter/widget/model/WidgetModel.kt | 35 ++++++------------- .../model/types/WidgetModelPropertyType.kt | 7 ++-- .../widget/model/types/compound/ArrayType.kt | 10 ++++-- .../model/types/compound/NullableType.kt | 10 ++++-- .../widget/model/types/datetime/DateType.kt | 7 ++-- .../model/types/datetime/DatetimeType.kt | 7 ++-- .../widget/model/types/datetime/TimeType.kt | 7 ++-- .../model/types/enums/WidgetEnumType.kt | 7 ++-- .../PrimitiveWidgetModelPropertyType.kt | 7 ++-- .../model/types/widget/WidgetReferenceType.kt | 12 +++---- 17 files changed, 86 insertions(+), 57 deletions(-) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt index 959522c..27a3f28 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt @@ -41,6 +41,7 @@ public class WidgetManager( private val widgetTarget = "jupyter.widget" private val widgetControlTarget = "jupyter.widget.control" private val widgets = mutableMapOf() + private val widgetIdByWidget = mutableMapOf() public val factoryRegistry: WidgetFactoryRegistry = WidgetFactoryRegistry() @@ -73,7 +74,7 @@ public class WidgetManager( val widget = widgetFactory.create(this) val patch = openMessage.toPatch(buffers) - widget.applyPatch(patch, this) + widget.applyPatch(patch) initializeWidget(comm, widget) } @@ -81,8 +82,10 @@ public class WidgetManager( public fun getWidget(modelId: String): WidgetModel? = widgets[modelId] + public fun getWidgetId(widget: WidgetModel): String? = widgetIdByWidget[widget] + public fun registerWidget(widget: WidgetModel) { - if (widget.id != null) return + if (widgetIdByWidget[widget] != null) return val fullState = widget.getFullState() val wireMessage = getWireMessage(fullState) @@ -107,7 +110,7 @@ public class WidgetManager( public fun renderWidget(widget: WidgetModel): DisplayResult = MimeTypedResultEx( buildJsonObject { - val modelId = widget.id ?: error("Widget is not registered") + val modelId = widgetIdByWidget[widget] ?: error("Widget is not registered") var versionMajor = DEFAULT_MAJOR_VERSION var versionMinor = DEFAULT_MINOR_VERSION var modelName: String? = null @@ -140,7 +143,7 @@ public class WidgetManager( widget: WidgetModel, ) { val modelId = comm.id - widget.setModelId(modelId) + widgetIdByWidget[widget] = modelId widgets[modelId] = widget // Reflect kernel-side changes on the frontend @@ -161,7 +164,7 @@ public class WidgetManager( comm.onMessage { msg, _, buffers -> when (val message = Json.decodeFromJsonElement(msg)) { is WidgetStateMessage -> { - widget.applyPatch(message.toPatch(buffers), this) + widget.applyPatch(message.toPatch(buffers)) } is RequestStateMessage -> { diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt index b0bf417..1da59db 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt @@ -13,7 +13,9 @@ public fun htmlWidget(): HtmlWidget = globalWidgetManager.html() private val spec = controlsSpec("HTML") -public class HtmlWidget internal constructor() : DefaultWidgetModel(spec) { +public class HtmlWidget internal constructor( + widgetManager: WidgetManager, +) : DefaultWidgetModel(spec, widgetManager) { internal object Factory : DefaultWidgetFactory(spec, ::HtmlWidget) public var value: String by stringProp("value") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt index aa46de4..77501e6 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt @@ -13,7 +13,9 @@ public fun intSliderWidget(): IntSliderWidget = globalWidgetManager.intSlider() private val spec = controlsSpec("IntSlider") -public class IntSliderWidget internal constructor() : DefaultWidgetModel(spec) { +public class IntSliderWidget internal constructor( + widgetManager: WidgetManager, +) : DefaultWidgetModel(spec, widgetManager) { internal object Factory : DefaultWidgetFactory(spec, ::IntSliderWidget) public var value: Int by intProp("value", 0) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt index 94ca5c2..272bd36 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt @@ -13,7 +13,9 @@ public fun labelWidget(): LabelWidget = globalWidgetManager.label() private val spec = controlsSpec("Label") -public class LabelWidget internal constructor() : DefaultWidgetModel(spec) { +public class LabelWidget internal constructor( + widgetManager: WidgetManager, +) : DefaultWidgetModel(spec, widgetManager) { internal object Factory : DefaultWidgetFactory(spec, ::LabelWidget) public var value: String by stringProp("value", "") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt index 8d81697..ac654ab 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt @@ -13,7 +13,9 @@ public fun layoutWidget(): LayoutWidget = globalWidgetManager.layout() private val spec = baseSpec("Layout") -public class LayoutWidget private constructor() : DefaultWidgetModel(spec) { +public class LayoutWidget private constructor( + widgetManager: WidgetManager, +) : DefaultWidgetModel(spec, widgetManager) { internal object Factory : DefaultWidgetFactory(spec, ::LayoutWidget) public var layout: String by stringProp("layout", "") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt index b417a47..59e0107 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt @@ -15,7 +15,7 @@ private val spec = outputSpec("Output") public class OutputWidget internal constructor( widgetManager: WidgetManager, -) : DefaultWidgetModel(spec) { +) : DefaultWidgetModel(spec, widgetManager) { internal object Factory : DefaultWidgetFactory(spec, ::OutputWidget) public var layout: LayoutWidget? by widgetProp("layout", widgetManager.layout()) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt index e95deb3..4318e10 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/ModelCreation.kt @@ -25,7 +25,8 @@ public abstract class DefaultWidgetFactory( public open class DefaultWidgetModel( spec: WidgetSpec, -) : WidgetModel() { + widgetManager: WidgetManager, +) : WidgetModel(widgetManager) { public val modelName: String by stringProp("_model_name", spec.modelName) public val modelModule: String by stringProp("_model_module", spec.modelModule) public val modelModuleVersion: String by stringProp("_model_module_version", spec.modelModuleVersion) diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt index 1887a2e..dac3af8 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/WidgetModel.kt @@ -12,26 +12,18 @@ import org.jetbrains.kotlinx.jupyter.widget.protocol.Patch import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -public abstract class WidgetModel { +public abstract class WidgetModel( + protected val widgetManager: WidgetManager, +) { private val properties = mutableMapOf>() private val changeListeners = mutableListOf<(Patch) -> Unit>() - private var _id: String? = null - public val id: String? get() = _id - - public fun setModelId(modelId: String) { - _id = modelId - } - public fun getFullState(): Patch = properties.mapValues { (_, property) -> property.serializedValue } - public fun applyPatch( - patch: Patch, - widgetManager: WidgetManager?, - ) { + public fun applyPatch(patch: Patch) { for ((key, value) in patch) { val property = properties[key] ?: continue - property.applyPatch(value, widgetManager) + property.applyPatch(value) } } @@ -92,7 +84,7 @@ public abstract class WidgetModel { private val property: WidgetModelProperty, ) : ReadWriteProperty { internal constructor(name: String, type: WidgetModelPropertyType, initialValue: T) : - this(WidgetModelPropertyImpl(name, type, initialValue)) + this(WidgetModelPropertyImpl(name, type, initialValue, widgetManager)) init { addProperty(property) @@ -120,10 +112,7 @@ public interface WidgetModelProperty { public val serializedValue: Any? - public fun applyPatch( - patch: Any?, - widgetManager: WidgetManager?, - ) + public fun applyPatch(patch: Any?) public fun addChangeListener(listener: (Any?) -> Unit) } @@ -132,6 +121,7 @@ internal class WidgetModelPropertyImpl( override val name: String, override val type: WidgetModelPropertyType, initialValue: T, + private val widgetManager: WidgetManager, ) : WidgetModelProperty { private var _value: T = initialValue private val listeners = mutableListOf<(Any?) -> Unit>() @@ -144,12 +134,9 @@ internal class WidgetModelPropertyImpl( notifyListeners(newValue) } - override val serializedValue: Any? get() = type.serialize(value) + override val serializedValue: Any? get() = type.serialize(value, widgetManager) - override fun applyPatch( - patch: Any?, - widgetManager: WidgetManager?, - ) { + override fun applyPatch(patch: Any?) { value = type.deserialize(patch, widgetManager) } @@ -158,7 +145,7 @@ internal class WidgetModelPropertyImpl( } private fun notifyListeners(newValue: T) { - val serializedValue = type.serialize(newValue) + val serializedValue = type.serialize(newValue, widgetManager) for (listener in listeners) { listener(serializedValue) } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt index 78fa35f..5c28411 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/WidgetModelPropertyType.kt @@ -6,10 +6,13 @@ public interface WidgetModelPropertyType { public val name: String public val default: T - public fun serialize(propertyValue: T): Any? + public fun serialize( + propertyValue: T, + widgetManager: WidgetManager, + ): Any? public fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): T } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt index 4c49d22..3411581 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/ArrayType.kt @@ -9,12 +9,18 @@ public class ArrayType( ) : AbstractWidgetModelPropertyType>("array<${elementType.name}>") { override val default: List = emptyList() - override fun serialize(propertyValue: List): List = propertyValue.map { elementType.serialize(it) } + override fun serialize( + propertyValue: List, + widgetManager: WidgetManager, + ): List = + propertyValue.map { + elementType.serialize(it, widgetManager) + } @Suppress("UNCHECKED_CAST") override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): List { require(patchValue is List<*>) { "Expected List for $name, got ${patchValue?.let { it::class.simpleName } ?: "null"}" diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt index 55ec7a5..c1890ca 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/compound/NullableType.kt @@ -9,10 +9,16 @@ public class NullableType( ) : AbstractWidgetModelPropertyType("${inner.name}?") { override val default: T? = null - override fun serialize(propertyValue: T?): Any? = propertyValue?.let { inner.serialize(it) } + override fun serialize( + propertyValue: T?, + widgetManager: WidgetManager, + ): Any? = + propertyValue?.let { + inner.serialize(it, widgetManager) + } override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): T? = if (patchValue == null) null else inner.deserialize(patchValue, widgetManager) } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt index 88b2017..aa0c8cd 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DateType.kt @@ -14,11 +14,14 @@ public object DateType : AbstractWidgetModelPropertyType("date") { .ofPattern("uuuu-MM-dd") .withResolverStyle(ResolverStyle.STRICT) - override fun serialize(propertyValue: LocalDate): Any? = propertyValue.format(formatter) + override fun serialize( + propertyValue: LocalDate, + widgetManager: WidgetManager, + ): Any? = propertyValue.format(formatter) override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): LocalDate { require(patchValue is String) { "Expected String for date, got ${patchValue?.let { it::class.simpleName } ?: "null"}" diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt index 91a25e1..e5ed0f2 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/DatetimeType.kt @@ -17,11 +17,14 @@ public object DatetimeType : AbstractWidgetModelPropertyType("datetime" .withResolverStyle(ResolverStyle.STRICT) .withZone(ZoneOffset.UTC) - override fun serialize(propertyValue: Instant): Any? = formatter.format(propertyValue) // respect 'Z' + override fun serialize( + propertyValue: Instant, + widgetManager: WidgetManager, + ): Any? = formatter.format(propertyValue) // respect 'Z' override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): Instant { require(patchValue is String) { "Expected String for datetime, got ${patchValue?.let { it::class.simpleName } ?: "null"}" diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt index 193b1ba..7748d9d 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/datetime/TimeType.kt @@ -14,11 +14,14 @@ public object TimeType : AbstractWidgetModelPropertyType("time") { .ofPattern("HH:mm:ss") .withResolverStyle(ResolverStyle.STRICT) - override fun serialize(propertyValue: LocalTime): Any? = propertyValue.format(formatter) + override fun serialize( + propertyValue: LocalTime, + widgetManager: WidgetManager, + ): Any? = propertyValue.format(formatter) override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): LocalTime { require(patchValue is String) { "Expected String for time, got ${patchValue?.let { it::class.simpleName } ?: "null"}" diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt index 5bfb934..563fbdc 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/enums/WidgetEnumType.kt @@ -7,11 +7,14 @@ public class WidgetEnumType>( private val widgetEnum: E, override val default: WidgetEnumEntry, ) : AbstractWidgetModelPropertyType>("enum") { - override fun serialize(propertyValue: WidgetEnumEntry): String = propertyValue.name + override fun serialize( + propertyValue: WidgetEnumEntry, + widgetManager: WidgetManager, + ): String = propertyValue.name override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): WidgetEnumEntry { require(patchValue is String) { "Expected String for enum, got ${patchValue?.let { it::class.simpleName } ?: "null"}" diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt index 376d8b4..a7919b4 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/primitive/PrimitiveWidgetModelPropertyType.kt @@ -7,11 +7,14 @@ public abstract class PrimitiveWidgetModelPropertyType( name: String, override val default: T, ) : AbstractWidgetModelPropertyType(name) { - override fun serialize(propertyValue: T): Any? = propertyValue + override fun serialize( + propertyValue: T, + widgetManager: WidgetManager, + ): Any? = propertyValue @Suppress("UNCHECKED_CAST") override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): T = patchValue as T } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt index b91b3b1..355944f 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/model/types/widget/WidgetReferenceType.kt @@ -9,21 +9,21 @@ private const val WIDGET_REF_PREFIX = "IPY_MODEL_" public class WidgetReferenceType : AbstractWidgetModelPropertyType("widget-ref") { override val default: M? get() = null - override fun serialize(propertyValue: M?): String? = + override fun serialize( + propertyValue: M?, + widgetManager: WidgetManager, + ): String? = propertyValue?.let { - "$WIDGET_REF_PREFIX${it.id}" + "$WIDGET_REF_PREFIX${widgetManager.getWidgetId(it)}" } @Suppress("UNCHECKED_CAST") override fun deserialize( patchValue: Any?, - widgetManager: WidgetManager?, + widgetManager: WidgetManager, ): M? { if (patchValue == null) return null - requireNotNull(widgetManager) { - "Widget manager is required to deserialize widget-ref" - } require(patchValue is String) { "Expected String for widget-ref, got ${patchValue::class.simpleName}" } From 48934a2ac7d254da89882fc2a7a670d22203aa4f Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Fri, 19 Dec 2025 02:31:50 +0100 Subject: [PATCH 5/6] KTNB-1205: Extract WidgetManager interface --- .../widget/WidgetJupyterIntegration.kt | 2 +- .../kotlinx/jupyter/widget/WidgetManager.kt | 185 +---------------- .../jupyter/widget/WidgetManagerImpl.kt | 190 ++++++++++++++++++ 3 files changed, 197 insertions(+), 180 deletions(-) create mode 100644 integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManagerImpl.kt diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt index 59bc106..9e8830c 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt @@ -14,7 +14,7 @@ public class WidgetJupyterIntegration : JupyterIntegration() { importPackage() var myLastClassLoader = WidgetJupyterIntegration::class.java.classLoader - val widgetManager = WidgetManager(notebook.commManager) { myLastClassLoader } + val widgetManager = WidgetManagerImpl(notebook.commManager) { myLastClassLoader } myWidgetManager = widgetManager onLoaded { diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt index 27a3f28..47031e3 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManager.kt @@ -1,190 +1,17 @@ package org.jetbrains.kotlinx.jupyter.widget -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.put import org.jetbrains.kotlinx.jupyter.api.DisplayResult -import org.jetbrains.kotlinx.jupyter.api.MimeTypedResultEx -import org.jetbrains.kotlinx.jupyter.api.MimeTypes -import org.jetbrains.kotlinx.jupyter.api.libraries.Comm -import org.jetbrains.kotlinx.jupyter.api.libraries.CommManager -import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MAJOR_VERSION -import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MINOR_VERSION -import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel import org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactoryRegistry import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel -import org.jetbrains.kotlinx.jupyter.widget.model.versionConstraintRegex -import org.jetbrains.kotlinx.jupyter.widget.protocol.CustomMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStateMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStatesMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.UpdateStatesMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetOpenMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetStateMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetUpdateMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.getWireMessage -import org.jetbrains.kotlinx.jupyter.widget.protocol.toPatch -private val widgetOpenMetadataJson = - buildJsonObject { - put("version", "${DEFAULT_MAJOR_VERSION}.${DEFAULT_MINOR_VERSION}") - } +public interface WidgetManager { + public val factoryRegistry: WidgetFactoryRegistry -public class WidgetManager( - private val commManager: CommManager, - private val classLoaderProvider: () -> ClassLoader, -) { - private val widgetTarget = "jupyter.widget" - private val widgetControlTarget = "jupyter.widget.control" - private val widgets = mutableMapOf() - private val widgetIdByWidget = mutableMapOf() + public fun getWidget(modelId: String): WidgetModel? - public val factoryRegistry: WidgetFactoryRegistry = WidgetFactoryRegistry() + public fun getWidgetId(widget: WidgetModel): String? - init { - commManager.registerCommTarget(widgetControlTarget) { comm, _, _, _ -> - comm.onMessage { msg, _, _ -> - when (Json.decodeFromJsonElement(msg)) { - is RequestStatesMessage -> { - val fullStates = - widgets.mapValues { (id, widget) -> - widget.getFullState() - } + public fun registerWidget(widget: WidgetModel) - val wireMessage = getWireMessage(fullStates) - val message = UpdateStatesMessage(wireMessage.state, wireMessage.bufferPaths) - - val data = Json.encodeToJsonElement(message).jsonObject - comm.send(data, null, emptyList()) - } - - else -> {} - } - } - } - - commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers -> - val openMessage = Json.decodeFromJsonElement(data) - val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!! - val widgetFactory = factoryRegistry.loadWidgetFactory(modelName, classLoaderProvider()) - - val widget = widgetFactory.create(this) - val patch = openMessage.toPatch(buffers) - widget.applyPatch(patch) - - initializeWidget(comm, widget) - } - } - - public fun getWidget(modelId: String): WidgetModel? = widgets[modelId] - - public fun getWidgetId(widget: WidgetModel): String? = widgetIdByWidget[widget] - - public fun registerWidget(widget: WidgetModel) { - if (widgetIdByWidget[widget] != null) return - - val fullState = widget.getFullState() - val wireMessage = getWireMessage(fullState) - - val comm = - commManager.openComm( - widgetTarget, - Json - .encodeToJsonElement( - WidgetOpenMessage( - wireMessage.state, - wireMessage.bufferPaths, - ), - ).jsonObject, - widgetOpenMetadataJson, - wireMessage.buffers, - ) - - initializeWidget(comm, widget) - } - - public fun renderWidget(widget: WidgetModel): DisplayResult = - MimeTypedResultEx( - buildJsonObject { - val modelId = widgetIdByWidget[widget] ?: error("Widget is not registered") - var versionMajor = DEFAULT_MAJOR_VERSION - var versionMinor = DEFAULT_MINOR_VERSION - var modelName: String? = null - if (widget is DefaultWidgetModel) { - modelName = widget.modelName - val version = widget.modelModuleVersion - val matchResult = versionConstraintRegex.find(version) - if (matchResult != null) { - versionMajor = matchResult.groupValues[1].toInt() - versionMinor = matchResult.groupValues[2].toInt() - } - } - if (modelName != null) { - put(MimeTypes.HTML, "$modelName(id=$modelId)") - } - put( - "application/vnd.jupyter.widget-view+json", - buildJsonObject { - put("version_major", versionMajor) - put("version_minor", versionMinor) - put("model_id", modelId) - }, - ) - }, - null, - ) - - private fun initializeWidget( - comm: Comm, - widget: WidgetModel, - ) { - val modelId = comm.id - widgetIdByWidget[widget] = modelId - widgets[modelId] = widget - - // Reflect kernel-side changes on the frontend - widget.addChangeListener { patch -> - val wireMessage = getWireMessage(patch) - val data = - Json - .encodeToJsonElement( - WidgetUpdateMessage( - wireMessage.state, - wireMessage.bufferPaths, - ), - ).jsonObject - comm.send(data, null, wireMessage.buffers) - } - - // Reflect frontend-side changes on kernel - comm.onMessage { msg, _, buffers -> - when (val message = Json.decodeFromJsonElement(msg)) { - is WidgetStateMessage -> { - widget.applyPatch(message.toPatch(buffers)) - } - - is RequestStateMessage -> { - val fullState = widget.getFullState() - val wireMessage = getWireMessage(fullState) - val data = - Json - .encodeToJsonElement( - WidgetUpdateMessage( - wireMessage.state, - wireMessage.bufferPaths, - ), - ).jsonObject - comm.send(data, null, wireMessage.buffers) - } - - is CustomMessage -> {} - - else -> {} - } - } - } + public fun renderWidget(widget: WidgetModel): DisplayResult } diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManagerImpl.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManagerImpl.kt new file mode 100644 index 0000000..5dcc755 --- /dev/null +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetManagerImpl.kt @@ -0,0 +1,190 @@ +package org.jetbrains.kotlinx.jupyter.widget + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import org.jetbrains.kotlinx.jupyter.api.DisplayResult +import org.jetbrains.kotlinx.jupyter.api.MimeTypedResultEx +import org.jetbrains.kotlinx.jupyter.api.MimeTypes +import org.jetbrains.kotlinx.jupyter.api.libraries.Comm +import org.jetbrains.kotlinx.jupyter.api.libraries.CommManager +import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MAJOR_VERSION +import org.jetbrains.kotlinx.jupyter.widget.model.DEFAULT_MINOR_VERSION +import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetFactoryRegistry +import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel +import org.jetbrains.kotlinx.jupyter.widget.model.versionConstraintRegex +import org.jetbrains.kotlinx.jupyter.widget.protocol.CustomMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStateMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.RequestStatesMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.UpdateStatesMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetOpenMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetStateMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.WidgetUpdateMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.getWireMessage +import org.jetbrains.kotlinx.jupyter.widget.protocol.toPatch + +private val widgetOpenMetadataJson = + buildJsonObject { + put("version", "${DEFAULT_MAJOR_VERSION}.${DEFAULT_MINOR_VERSION}") + } + +public class WidgetManagerImpl( + private val commManager: CommManager, + private val classLoaderProvider: () -> ClassLoader, +) : WidgetManager { + private val widgetTarget = "jupyter.widget" + private val widgetControlTarget = "jupyter.widget.control" + private val widgets = mutableMapOf() + private val widgetIdByWidget = mutableMapOf() + + override val factoryRegistry: WidgetFactoryRegistry = WidgetFactoryRegistry() + + init { + commManager.registerCommTarget(widgetControlTarget) { comm, _, _, _ -> + comm.onMessage { msg, _, _ -> + when (Json.decodeFromJsonElement(msg)) { + is RequestStatesMessage -> { + val fullStates = + widgets.mapValues { (id, widget) -> + widget.getFullState() + } + + val wireMessage = getWireMessage(fullStates) + val message = UpdateStatesMessage(wireMessage.state, wireMessage.bufferPaths) + + val data = Json.encodeToJsonElement(message).jsonObject + comm.send(data, null, emptyList()) + } + + else -> {} + } + } + } + + commManager.registerCommTarget(widgetTarget) { comm, data, _, buffers -> + val openMessage = Json.decodeFromJsonElement(data) + val modelName = openMessage.state["_model_name"]?.jsonPrimitive?.content!! + val widgetFactory = factoryRegistry.loadWidgetFactory(modelName, classLoaderProvider()) + + val widget = widgetFactory.create(this) + val patch = openMessage.toPatch(buffers) + widget.applyPatch(patch) + + initializeWidget(comm, widget) + } + } + + override fun getWidget(modelId: String): WidgetModel? = widgets[modelId] + + override fun getWidgetId(widget: WidgetModel): String? = widgetIdByWidget[widget] + + override fun registerWidget(widget: WidgetModel) { + if (getWidgetId(widget) != null) return + + val fullState = widget.getFullState() + val wireMessage = getWireMessage(fullState) + + val comm = + commManager.openComm( + widgetTarget, + Json + .encodeToJsonElement( + WidgetOpenMessage( + wireMessage.state, + wireMessage.bufferPaths, + ), + ).jsonObject, + widgetOpenMetadataJson, + wireMessage.buffers, + ) + + initializeWidget(comm, widget) + } + + override fun renderWidget(widget: WidgetModel): DisplayResult = + MimeTypedResultEx( + buildJsonObject { + val modelId = getWidgetId(widget) ?: error("Widget is not registered") + var versionMajor = DEFAULT_MAJOR_VERSION + var versionMinor = DEFAULT_MINOR_VERSION + var modelName: String? = null + if (widget is DefaultWidgetModel) { + modelName = widget.modelName + val version = widget.modelModuleVersion + val matchResult = versionConstraintRegex.find(version) + if (matchResult != null) { + versionMajor = matchResult.groupValues[1].toInt() + versionMinor = matchResult.groupValues[2].toInt() + } + } + if (modelName != null) { + put(MimeTypes.HTML, "$modelName(id=$modelId)") + } + put( + "application/vnd.jupyter.widget-view+json", + buildJsonObject { + put("version_major", versionMajor) + put("version_minor", versionMinor) + put("model_id", modelId) + }, + ) + }, + null, + ) + + private fun initializeWidget( + comm: Comm, + widget: WidgetModel, + ) { + val modelId = comm.id + widgetIdByWidget[widget] = modelId + widgets[modelId] = widget + + // Reflect kernel-side changes on the frontend + widget.addChangeListener { patch -> + val wireMessage = getWireMessage(patch) + val data = + Json + .encodeToJsonElement( + WidgetUpdateMessage( + wireMessage.state, + wireMessage.bufferPaths, + ), + ).jsonObject + comm.send(data, null, wireMessage.buffers) + } + + // Reflect frontend-side changes on kernel + comm.onMessage { msg, _, buffers -> + when (val message = Json.decodeFromJsonElement(msg)) { + is WidgetStateMessage -> { + widget.applyPatch(message.toPatch(buffers)) + } + + is RequestStateMessage -> { + val fullState = widget.getFullState() + val wireMessage = getWireMessage(fullState) + val data = + Json + .encodeToJsonElement( + WidgetUpdateMessage( + wireMessage.state, + wireMessage.bufferPaths, + ), + ).jsonObject + comm.send(data, null, wireMessage.buffers) + } + + is CustomMessage -> {} + + else -> {} + } + } + } +} From 33cb8ca094f97c4a73b058692a10c0d79ed9b538 Mon Sep 17 00:00:00 2001 From: Ilya Muradyan Date: Fri, 19 Dec 2025 09:46:52 +0100 Subject: [PATCH 6/6] KTNB-1205: Add separate integration module and README --- README.md | 1 + integrations/widgets/README.md | 35 +++++++++++++++++++ .../widgets/widgets-api/build.gradle.kts | 7 ---- .../jupyter/widget/library/HtmlWidget.kt | 3 -- .../jupyter/widget/library/IntSliderWidget.kt | 3 -- .../jupyter/widget/library/LabelWidget.kt | 3 -- .../jupyter/widget/library/LayoutWidget.kt | 3 -- .../jupyter/widget/library/OutputWidget.kt | 3 -- .../widgets/widgets-jupyter/build.gradle.kts | 29 +++++++++++++++ .../integration/JupyterWidgetLibrary.kt | 22 ++++++++++++ .../integration}/WidgetJupyterIntegration.kt | 6 ++-- settings.gradle.kts | 1 + 12 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 integrations/widgets/README.md create mode 100644 integrations/widgets/widgets-jupyter/build.gradle.kts create mode 100644 integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/JupyterWidgetLibrary.kt rename integrations/widgets/{widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget => widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration}/WidgetJupyterIntegration.kt (85%) diff --git a/README.md b/README.md index 07cac28..010df89 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Each integration lives in the `integrations/` directory and has its own README. - [IntelliJ Platform](integrations/intellij-platform/README.md) — interactive access to IntelliJ Platform APIs from a running IDE process. - [HTTP Utilities](integrations/http-util/README.md) — JSON/serialization helpers and Ktor HTTP client wrappers for notebooks. - [Database](integrations/database/README.md) — helpers for configuring JDBC DataSources and working with Databases. +- [Widgets](integrations/widgets/README.md) — ipywidgets API for Kotlin notebooks. ## Contributing diff --git a/integrations/widgets/README.md b/integrations/widgets/README.md new file mode 100644 index 0000000..7b38523 --- /dev/null +++ b/integrations/widgets/README.md @@ -0,0 +1,35 @@ +[![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) +[![Kotlin experimental stability](https://img.shields.io/badge/project-experimental-kotlin.svg?colorA=555555&colorB=AC29EC&label=&logo=kotlin&logoColor=ffffff&logoWidth=10)](https://kotlinlang.org/docs/components-stability.html) + +# Kotlin Notebook Widgets Integration + +This integration provides a collection of interactive widgets for Kotlin Notebooks, such as sliders, labels, and more. It allows you to create a richer, more interactive experience within your notebooks. + +## Usage + +Use this API through the `%use widgets` magic command in a Kotlin Notebook. + +```kotlin +%use widgets + +val slider = intSliderWidget().apply { + min = 0 + max = 100 + value = 50 + description = "Select a value:" +} + +val label = labelWidget().apply { + value = "Current value: ${slider.value}" +} + +// Display the slider +slider +``` + +## Module structure + +This project consists of the following modules: + +- `widgets-api`: Contains the core widget implementations, protocols, and model definitions. +- `widgets-jupyter`: Provides the integration logic and useful helpers for Kotlin Jupyter notebooks. diff --git a/integrations/widgets/widgets-api/build.gradle.kts b/integrations/widgets/widgets-api/build.gradle.kts index a4ebbe0..cdde7f4 100644 --- a/integrations/widgets/widgets-api/build.gradle.kts +++ b/integrations/widgets/widgets-api/build.gradle.kts @@ -8,9 +8,6 @@ plugins { dependencies { compileOnly(libs.kotlinx.serialization.json) implementation(libs.kotlin.reflect) - - testImplementation(libs.kotlin.test) - testImplementation(libs.kotlinx.serialization.json) } kotlin { @@ -22,10 +19,6 @@ kotlin { explicitApi() } -tasks.processJupyterApiResources { - libraryProducers = listOf("org.jetbrains.kotlinx.jupyter.widget.WidgetJupyterIntegration") -} - kotlinPublications { publication { description.set("Kotlin APIs for IPython Widgets") diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt index 1da59db..45cd67a 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/HtmlWidget.kt @@ -1,7 +1,6 @@ package org.jetbrains.kotlinx.jupyter.widget.library import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec @@ -9,8 +8,6 @@ import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget public fun WidgetManager.html(): HtmlWidget = createAndRegisterWidget(HtmlWidget.Factory) -public fun htmlWidget(): HtmlWidget = globalWidgetManager.html() - private val spec = controlsSpec("HTML") public class HtmlWidget internal constructor( diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt index 77501e6..3baf9eb 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/IntSliderWidget.kt @@ -1,7 +1,6 @@ package org.jetbrains.kotlinx.jupyter.widget.library import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec @@ -9,8 +8,6 @@ import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget public fun WidgetManager.intSlider(): IntSliderWidget = createAndRegisterWidget(IntSliderWidget.Factory) -public fun intSliderWidget(): IntSliderWidget = globalWidgetManager.intSlider() - private val spec = controlsSpec("IntSlider") public class IntSliderWidget internal constructor( diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt index 272bd36..68172b8 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LabelWidget.kt @@ -1,7 +1,6 @@ package org.jetbrains.kotlinx.jupyter.widget.library import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel import org.jetbrains.kotlinx.jupyter.widget.model.controlsSpec @@ -9,8 +8,6 @@ import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget public fun WidgetManager.label(): LabelWidget = createAndRegisterWidget(LabelWidget.Factory) -public fun labelWidget(): LabelWidget = globalWidgetManager.label() - private val spec = controlsSpec("Label") public class LabelWidget internal constructor( diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt index ac654ab..ada61e7 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/LayoutWidget.kt @@ -1,7 +1,6 @@ package org.jetbrains.kotlinx.jupyter.widget.library import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel import org.jetbrains.kotlinx.jupyter.widget.model.baseSpec @@ -9,8 +8,6 @@ import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget public fun WidgetManager.layout(): LayoutWidget = createAndRegisterWidget(LayoutWidget.Factory) -public fun layoutWidget(): LayoutWidget = globalWidgetManager.layout() - private val spec = baseSpec("Layout") public class LayoutWidget private constructor( diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt index 59e0107..76cb5a7 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt +++ b/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/library/OutputWidget.kt @@ -1,7 +1,6 @@ package org.jetbrains.kotlinx.jupyter.widget.library import org.jetbrains.kotlinx.jupyter.widget.WidgetManager -import org.jetbrains.kotlinx.jupyter.widget.globalWidgetManager import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetFactory import org.jetbrains.kotlinx.jupyter.widget.model.DefaultWidgetModel import org.jetbrains.kotlinx.jupyter.widget.model.createAndRegisterWidget @@ -9,8 +8,6 @@ import org.jetbrains.kotlinx.jupyter.widget.model.outputSpec public fun WidgetManager.output(): OutputWidget = createAndRegisterWidget(OutputWidget.Factory) -public fun outputWidget(): OutputWidget = globalWidgetManager.output() - private val spec = outputSpec("Output") public class OutputWidget internal constructor( diff --git a/integrations/widgets/widgets-jupyter/build.gradle.kts b/integrations/widgets/widgets-jupyter/build.gradle.kts new file mode 100644 index 0000000..46de369 --- /dev/null +++ b/integrations/widgets/widgets-jupyter/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.publisher) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.jupyter.api) +} + +dependencies { + implementation(projects.integrations.widgets.widgetsApi) +} + +kotlin { + jvmToolchain( + libs.versions.jvm.toolchain + .get() + .toInt(), + ) + explicitApi() +} + +tasks.processJupyterApiResources { + libraryProducers = listOf("org.jetbrains.kotlinx.jupyter.widget.integration.WidgetJupyterIntegration") +} + +kotlinPublications { + publication { + description.set("Kotlin Jupyter kernel integration for IPython Widgets") + } +} diff --git a/integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/JupyterWidgetLibrary.kt b/integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/JupyterWidgetLibrary.kt new file mode 100644 index 0000000..10d6620 --- /dev/null +++ b/integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/JupyterWidgetLibrary.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlinx.jupyter.widget.integration + +import org.jetbrains.kotlinx.jupyter.widget.library.HtmlWidget +import org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget +import org.jetbrains.kotlinx.jupyter.widget.library.LabelWidget +import org.jetbrains.kotlinx.jupyter.widget.library.LayoutWidget +import org.jetbrains.kotlinx.jupyter.widget.library.OutputWidget +import org.jetbrains.kotlinx.jupyter.widget.library.html +import org.jetbrains.kotlinx.jupyter.widget.library.intSlider +import org.jetbrains.kotlinx.jupyter.widget.library.label +import org.jetbrains.kotlinx.jupyter.widget.library.layout +import org.jetbrains.kotlinx.jupyter.widget.library.output + +public fun htmlWidget(): HtmlWidget = globalWidgetManager.html() + +public fun intSliderWidget(): IntSliderWidget = globalWidgetManager.intSlider() + +public fun labelWidget(): LabelWidget = globalWidgetManager.label() + +public fun layoutWidget(): LayoutWidget = globalWidgetManager.layout() + +public fun outputWidget(): OutputWidget = globalWidgetManager.output() diff --git a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt b/integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/WidgetJupyterIntegration.kt similarity index 85% rename from integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt rename to integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/WidgetJupyterIntegration.kt index 9e8830c..224803a 100644 --- a/integrations/widgets/widgets-api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/WidgetJupyterIntegration.kt +++ b/integrations/widgets/widgets-jupyter/src/main/kotlin/org/jetbrains/kotlinx/jupyter/widget/integration/WidgetJupyterIntegration.kt @@ -1,8 +1,9 @@ -package org.jetbrains.kotlinx.jupyter.widget +package org.jetbrains.kotlinx.jupyter.widget.integration import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration -import org.jetbrains.kotlinx.jupyter.widget.library.IntSliderWidget +import org.jetbrains.kotlinx.jupyter.widget.WidgetManager +import org.jetbrains.kotlinx.jupyter.widget.WidgetManagerImpl import org.jetbrains.kotlinx.jupyter.widget.model.WidgetModel private var myWidgetManager: WidgetManager? = null @@ -11,7 +12,6 @@ internal val globalWidgetManager: WidgetManager get() = myWidgetManager!! public class WidgetJupyterIntegration : JupyterIntegration() { override fun Builder.onLoaded() { importPackage() - importPackage() var myLastClassLoader = WidgetJupyterIntegration::class.java.classLoader val widgetManager = WidgetManagerImpl(notebook.commManager) { myLastClassLoader } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5826c58..d53ac93 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ projectStructure { project("intellij-platform") folder("widgets") { project("widgets-api") + project("widgets-jupyter") } } }