diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dc763b0f1..2289703bc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,6 +8,12 @@ jobs:
assemble:
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ supported-ij-version:
+ - 232
+ - 233
+
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
@@ -20,11 +26,17 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run :assemble task
- run: ./gradlew assemble
+ run: ./gradlew assemble -Psupported.ij.version=${{ matrix.supported-ij-version }}
checks:
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ supported-ij-version:
+ - 232
+ - 233
+
steps:
- uses: actions/checkout@v3
@@ -39,11 +51,11 @@ jobs:
run: chmod +x gradlew
- name: Run :check task
- run: ./gradlew check --continue
+ run: ./gradlew check -Psupported.ij.version=${{ matrix.supported-ij-version }} --continue
- name: Merge SARIF reports
# Necessary because upload-sarif only takes up to 15 SARIF files and we have more
- run: ./gradlew :mergeSarifReports
+ run: ./gradlew :mergeSarifReports -Psupported.ij.version=${{ matrix.supported-ij-version }}
if: ${{ always() }}
- uses: github/codeql-action/upload-sarif@v2
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 376b39f3f..988c8359a 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -5,9 +5,10 @@ on:
push:
branches: [ main ]
jobs:
- publish:
- name: Publish
+ publish-core:
+ name: Publish Jewel Core
runs-on: ubuntu-latest
+
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
@@ -17,7 +18,33 @@ jobs:
- name: Setup Gradle
run: chmod +x gradlew
- name: Run Gradle
- run: ./gradlew publishAllPublicationsToSpaceRepository
+ # supported.ij.version is needed here for project configuration and won't be used during publishing
+ run: ./gradlew publishMainPublicationToSpaceRepository -Psupported.ij.version=232
+ env:
+ MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
+ MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
+
+ publish-ide:
+ name: Publish Jewel IDE part
+ needs: publish-core
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ supported-ij-version:
+ - 232
+ - 233
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 17
+ - name: Setup Gradle
+ run: chmod +x gradlew
+ - name: Run Gradle
+ run: ./gradlew publishIdeMainPublicationToSpaceRepository -Psupported.ij.version=${{ matrix.supported-ij-version }}
env:
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 073a9ce90..fd1904ab3 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -11,7 +11,6 @@
-
diff --git a/LICENSE b/LICENSE
index d2876367b..b0ed66dd3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2022 JetBrains
+ Copyright 2022–3 JetBrains s.r.o.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 66bbf1061..64cb06248 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-[![JetBrains incubator](https://camo.githubusercontent.com/be6f8b50b2400e8b0dc74e58dd9a68803fe6698f5f30d843a7504888879f8392/68747470733a2f2f6a622e67672f6261646765732f696e63756261746f722d706c61737469632e737667)](https://github.com/JetBrains#jetbrains-on-github) [![CI checks](https://github.com/JetBrains/jewel/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/JetBrains/jewel/actions/workflows/build.yml)
+[![JetBrains incubator](https://img.shields.io/badge/JetBrains-incubator-yellow)](https://github.com/JetBrains#jetbrains-on-github) [![CI checks](https://img.shields.io/github/actions/workflow/status/JetBrains/jewel/build.yml?logo=github)](https://github.com/JetBrains/jewel/actions/workflows/build.yml) [![Licensed under Apache 2.0](https://img.shields.io/github/license/JetBrains/jewel)](https://github.com/JetBrains/jewel/blob/main/LICENSE) [![Latest release](https://img.shields.io/github/v/release/JetBrains/jewel?include_prereleases&label=Latest%20Release&logo=github)](https://github.com/JetBrains/jewel/releases/latest)
+
# Jewel: a Compose for Desktop theme
@@ -9,10 +10,11 @@ desktop-optimized theme and set of components.
> [!WARNING]
>
-> This project is in very early development and is probably not ready to be used in production projects. You _can_, but
-> you should expect APIs to change fairly often, things to move around and/or break, and all that jazz.
+> This project is in active development, and caution is advised when considering it for production uses. You _can_,
+> but you should expect APIs to change often, things to move around and/or break, and all that jazz. Binary
+> compatibility is not currently guaranteed across releases, but it is an eventual aim for 1.0, if it is possible.
>
-> Use at your risk!
+> Use at your own risk! (but have fun if you do!)
Jewel provides stand-alone implementations of the IntelliJ Platform themes that can be used in any Compose for Desktop
application, and a Swing LaF Bridge that only works in the IntelliJ Platform (i.e., used to create IDE plugins), but
@@ -90,3 +92,22 @@ you, but if you want to also enable it in other scenarios and in standalone appl
You can find help on the [`#jewel`](https://app.slack.com/client/T09229ZC6/C05T8U2C31T) channel on the Kotlin Slack.
If you don't already have access to the Kotlin Slack, you can request it
[here](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up).
+
+## License
+Jewel is licensed under the [Apache 2.0 license](https://github.com/JetBrains/jewel/blob/main/LICENSE).
+
+```
+ Copyright 2022–3 JetBrains s.r.o.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 74cbd3f0b..345c3aaf5 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -9,6 +9,10 @@ gradlePlugin {
id = "intellij-theme-generator"
implementationClass = "org.jetbrains.jewel.buildlogic.theme.IntelliJThemeGeneratorPlugin"
}
+ register("android-studio-releases-generator") {
+ id = "android-studio-releases-generator"
+ implementationClass = "org.jetbrains.jewel.buildlogic.demodata.AndroidStudioReleasesGeneratorPlugin"
+ }
}
}
diff --git a/buildSrc/src/main/kotlin/IdeaConfiguration.kt b/buildSrc/src/main/kotlin/IdeaConfiguration.kt
new file mode 100644
index 000000000..ecdb2e735
--- /dev/null
+++ b/buildSrc/src/main/kotlin/IdeaConfiguration.kt
@@ -0,0 +1,37 @@
+import java.util.concurrent.atomic.AtomicBoolean
+import org.gradle.api.Project
+
+enum class SupportedIJVersion {
+ IJ_232,
+ IJ_233
+}
+
+private var warned = AtomicBoolean(false)
+
+fun Project.supportedIJVersion(): SupportedIJVersion {
+ val prop = kotlin.runCatching {
+ localProperty("supported.ij.version")
+ ?: rootProject.property("supported.ij.version")?.toString()
+ }.getOrNull()
+
+ if (prop == null) {
+ if (!warned.getAndSet(true)) {
+ logger.warn(
+ """
+ No 'supported.ij.version' property provided. Falling back to IJ 233.
+ It is recommended to provide it using local.properties file or -Psupported.ij.version to avoid unexpected behavior.
+ """.trimIndent()
+ )
+ }
+ return SupportedIJVersion.IJ_233
+ }
+
+ return when (prop) {
+ "232" -> SupportedIJVersion.IJ_232
+ "233" -> SupportedIJVersion.IJ_233
+ else -> error(
+ "Invalid 'supported.ij.version' with value '$prop' is provided. " +
+ "It should be in set of these values: ('232', '233')"
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/LocalProperties.kt b/buildSrc/src/main/kotlin/LocalProperties.kt
new file mode 100644
index 000000000..526aa08f3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/LocalProperties.kt
@@ -0,0 +1,12 @@
+import org.gradle.api.Project
+import java.util.Properties
+
+internal fun Project.localProperty(propertyName: String): String? {
+ val localPropertiesFile = rootProject.file("local.properties")
+ if (!localPropertiesFile.exists()) {
+ return null
+ }
+ val properties = Properties()
+ localPropertiesFile.inputStream().use { properties.load(it) }
+ return properties.getProperty(propertyName)
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/PublishConfiguration.kt b/buildSrc/src/main/kotlin/PublishConfiguration.kt
new file mode 100644
index 000000000..b2ea7d2f8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/PublishConfiguration.kt
@@ -0,0 +1,35 @@
+@file:Suppress("UnstableApiUsage")
+
+import org.gradle.api.publish.PublishingExtension
+import org.gradle.api.publish.maven.MavenPom
+import org.gradle.kotlin.dsl.assign
+import org.gradle.kotlin.dsl.maven
+
+internal fun PublishingExtension.configureJewelRepositories() {
+ repositories {
+ maven("https://packages.jetbrains.team/maven/p/kpm/public") {
+ name = "Space"
+ credentials {
+ username = System.getenv("MAVEN_SPACE_USERNAME")
+ password = System.getenv("MAVEN_SPACE_PASSWORD")
+ }
+ }
+ }
+}
+
+internal fun MavenPom.configureJewelPom() {
+ name = "Jewel"
+ description = "A theme for Compose for Desktop that implements the IntelliJ Platform look and feel."
+ url = "https://github.com/JetBrains/jewel"
+ licenses {
+ license {
+ name = "Apache License 2.0"
+ url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+ }
+ }
+ scm {
+ connection = "scm:git:https://github.com/JetBrains/jewel.git"
+ developerConnection = "scm:git:https://github.com/JetBrains/jewel.git"
+ url = "https://github.com/JetBrains/jewel"
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/jewel-ij-publish.gradle.kts b/buildSrc/src/main/kotlin/jewel-ij-publish.gradle.kts
new file mode 100644
index 000000000..1219a5a1b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/jewel-ij-publish.gradle.kts
@@ -0,0 +1,63 @@
+@file:Suppress("UnstableApiUsage")
+
+import SupportedIJVersion.IJ_232
+import SupportedIJVersion.IJ_233
+
+plugins {
+ kotlin("jvm")
+ `maven-publish`
+ id("org.jetbrains.dokka")
+}
+
+val sourcesJar by tasks.registering(Jar::class) {
+ from(kotlin.sourceSets.main.map { it.kotlin })
+ archiveClassifier = "sources"
+ destinationDirectory = layout.buildDirectory.dir("artifacts")
+}
+
+val javadocJar by tasks.registering(Jar::class) {
+ from(tasks.dokkaHtml)
+ archiveClassifier = "javadoc"
+ destinationDirectory = layout.buildDirectory.dir("artifacts")
+}
+
+publishing {
+ configureJewelRepositories()
+
+ publications {
+ register("IdeMain") {
+ from(components["kotlin"])
+ artifact(javadocJar)
+ artifact(sourcesJar)
+ val ijVersionRaw = when (supportedIJVersion()) {
+ IJ_232 -> "232"
+ IJ_233 -> "233"
+ }
+ version = project.version.toString().withVersionSuffix("ij-$ijVersionRaw")
+ artifactId = "jewel-${project.name}"
+ pom {
+ configureJewelPom()
+ }
+ }
+ }
+}
+
+/**
+ * Adds suffix to the version taking SNAPSHOT suffix into account
+ *
+ * For example, if [this] is "0.0.1-SNAPSHOT" and [suffix] is "ij-233"
+ * then result will be "0.0.1-ij-233-SNAPSHOT"
+ */
+fun String.withVersionSuffix(suffix: String): String {
+ val splitString = this.split('-')
+ val snapshotRaw = "SNAPSHOT"
+ val withSnapshot = splitString.contains(snapshotRaw)
+
+ if (!withSnapshot) {
+ return "$this-$suffix"
+ }
+
+ val withoutSnapshot = splitString.filter { it != snapshotRaw }.joinToString("-")
+
+ return "$withoutSnapshot-$suffix-$snapshotRaw"
+}
diff --git a/buildSrc/src/main/kotlin/jewel-publish.gradle.kts b/buildSrc/src/main/kotlin/jewel-publish.gradle.kts
index e7a20b84e..b41f71b36 100644
--- a/buildSrc/src/main/kotlin/jewel-publish.gradle.kts
+++ b/buildSrc/src/main/kotlin/jewel-publish.gradle.kts
@@ -19,15 +19,8 @@ val javadocJar by tasks.registering(Jar::class) {
}
publishing {
- repositories {
- maven("https://packages.jetbrains.team/maven/p/kpm/public") {
- name = "Space"
- credentials {
- username = System.getenv("MAVEN_SPACE_USERNAME")
- password = System.getenv("MAVEN_SPACE_PASSWORD")
- }
- }
- }
+ configureJewelRepositories()
+
publications {
register("main") {
from(components["kotlin"])
@@ -36,20 +29,7 @@ publishing {
version = project.version.toString()
artifactId = "jewel-${project.name}"
pom {
- name = "Jewel"
- description = "intelliJ theming system in for Compose."
- url = "https://github.com/JetBrains/jewel"
- licenses {
- license {
- name = "Apache License 2.0"
- url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
- }
- }
- scm {
- connection = "scm:git:https://github.com/JetBrains/jewel.git"
- developerConnection = "scm:git:https://github.com/JetBrains/jewel.git"
- url = "https://github.com/JetBrains/jewel"
- }
+ configureJewelPom()
}
}
}
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleasesGeneratorPlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleasesGeneratorPlugin.kt
new file mode 100644
index 000000000..3647105de
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleasesGeneratorPlugin.kt
@@ -0,0 +1,117 @@
+package org.jetbrains.jewel.buildlogic.demodata
+
+import com.squareup.kotlinpoet.ClassName
+import gradle.kotlin.dsl.accessors._c011fd04eb69b06af6f445fec200c5f6.main
+import gradle.kotlin.dsl.accessors._c011fd04eb69b06af6f445fec200c5f6.sourceSets
+import io.gitlab.arturbosch.detekt.Detekt
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import org.gradle.api.DefaultTask
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.SetProperty
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.kotlin.dsl.get
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.property
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.setProperty
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
+import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
+import java.io.File
+import java.net.URL
+
+abstract class AndroidStudioReleasesGeneratorPlugin : Plugin {
+
+ final override fun apply(target: Project) {
+ with(target) {
+ val extension = extensions.findByType(StudioVersionsGenerationExtension::class.java)
+ ?: extensions.create("androidStudioReleasesGenerator", StudioVersionsGenerationExtension::class.java)
+
+ val task =
+ tasks.register("generateAndroidStudioReleasesList") {
+ val className = ClassName.bestGuess(OUTPUT_CLASS_NAME)
+ outputFile.set(
+ extension.targetDir.file(
+ className.packageName.replace(".", "/")
+ .plus("/${className.simpleName}.kt")
+ )
+ )
+ dataUrl.set(extension.dataUrl)
+ resourcesDirs.set(extension.resourcesDirs)
+ }
+ tasks.withType {
+ dependsOn(task)
+ }
+ tasks.withType {
+ dependsOn(task)
+ }
+ pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
+ extensions.getByType().apply {
+ sourceSets["main"].kotlin.srcDir(extension.targetDir)
+ }
+ }
+ }
+ }
+}
+
+open class StudioVersionsGenerationExtension(project: Project) {
+
+ val targetDir: DirectoryProperty = project.objects.directoryProperty()
+ .convention(project.layout.buildDirectory.dir("generated/studio-releases/"))
+
+ val resourcesDirs: SetProperty = project.objects.setProperty()
+ .convention(project.sourceSets.main.get().resources.srcDirs)
+
+ val dataUrl: Property = project.objects.property()
+ .convention("https://jb.gg/android-studio-releases-list.json")
+}
+
+private const val OUTPUT_CLASS_NAME = "org.jetbrains.jewel.samples.ideplugin.releasessample.AndroidStudioReleases"
+
+open class AndroidStudioReleasesGeneratorTask : DefaultTask() {
+
+ @get:OutputFile
+ val outputFile: RegularFileProperty = project.objects.fileProperty()
+
+ @get:Input
+ val dataUrl = project.objects.property()
+
+ @get:Input
+ val resourcesDirs = project.objects.setProperty()
+
+ init {
+ group = "jewel"
+ }
+
+ @TaskAction
+ fun generate() {
+ val json = Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ }
+ val url = dataUrl.get()
+ val lookupDirs = resourcesDirs.get()
+
+ logger.lifecycle("Fetching Android Studio releases list from $url...")
+ logger.debug(
+ "Registered resources directories:\n" +
+ lookupDirs.joinToString("\n") { " * ${it.absolutePath}" }
+ )
+ val releases = URL(url).openStream()
+ .use { json.decodeFromStream(it) }
+
+ val className = ClassName.bestGuess(OUTPUT_CLASS_NAME)
+ val file = AndroidStudioReleasesReader.readFrom(releases, className, url, lookupDirs)
+
+ val outputFile = outputFile.get().asFile
+ outputFile.bufferedWriter().use { file.writeTo(it) }
+ logger.lifecycle("Android Studio releases from $url parsed and code generated into ${outputFile.path}")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleasesReader.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleasesReader.kt
new file mode 100644
index 000000000..77fdc4bed
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleasesReader.kt
@@ -0,0 +1,155 @@
+package org.jetbrains.jewel.buildlogic.demodata
+
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asClassName
+import com.squareup.kotlinpoet.joinToCode
+import java.io.File
+import java.time.ZonedDateTime
+
+private val ContentItemClassName =
+ ClassName.bestGuess("org.jetbrains.jewel.samples.ideplugin.releasessample.ContentItem.AndroidStudio")
+
+internal object AndroidStudioReleasesReader {
+
+ fun readFrom(
+ releases: ApiAndroidStudioReleases,
+ className: ClassName,
+ url: String,
+ resourceDirs: Set,
+ ) =
+ FileSpec.builder(className).apply {
+ indent(" ")
+ addFileComment("Generated by the Jewel Android Studio Releases Generator\n")
+ addFileComment("Generated from $url on ${ZonedDateTime.now()}\n")
+
+ addImport("org.jetbrains.jewel.samples.ideplugin.releasessample", "ContentItem.AndroidStudio")
+ addImport("kotlinx.datetime", "LocalDate")
+
+ addType(
+ TypeSpec.objectBuilder(className)
+ .superclass(
+ ClassName.bestGuess("org.jetbrains.jewel.samples.ideplugin.releasessample.ContentSource")
+ .parameterizedBy(ContentItemClassName)
+ )
+ .apply {
+ addProperty(
+ PropertySpec.builder(
+ name = "items",
+ type = List::class.asClassName().parameterizedBy(ContentItemClassName),
+ KModifier.OVERRIDE
+ )
+ .initializer(readReleases(releases, resourceDirs))
+ .build()
+ )
+
+ addProperty(
+ PropertySpec.builder(
+ "displayName",
+ type = String::class.asClassName(),
+ KModifier.OVERRIDE
+ )
+ .initializer("\"%L\"", "Android Studio releases")
+ .build()
+ )
+ }.build()
+ )
+ }.build()
+
+ private fun readReleases(releases: ApiAndroidStudioReleases, resourceDirs: Set) =
+ releases.content.item
+ .map { readRelease(it, resourceDirs) }
+ .joinToCode(prefix = "listOf(\n", separator = ",\n", suffix = ")")
+
+ private fun readRelease(release: ApiAndroidStudioReleases.Content.Item, resourceDirs: Set) =
+ CodeBlock.builder()
+ .apply {
+ add("AndroidStudio(\n")
+ add(" displayText = \"%L\",\n", release.name)
+ add(" imagePath = %L,\n", imagePathForOrNull(release, resourceDirs))
+ add(" versionName = \"%L\",\n", release.version)
+ add(" build = \"%L\",\n", release.build)
+ add(" platformBuild = \"%L\",\n", release.platformBuild)
+ add(" platformVersion = \"%L\",\n", release.platformVersion)
+ add(" channel = %L,\n", readChannel(release.channel))
+ add(" releaseDate = LocalDate(%L),\n", translateDate(release.date))
+ add(" key = \"%L\",\n", release.build)
+ add(")")
+ }
+ .build()
+
+ private fun imagePathForOrNull(release: ApiAndroidStudioReleases.Content.Item, resourceDirs: Set): String? {
+ // Take the release animal from the name, remove spaces and voila'
+ val releaseAnimal = release.name
+ .substringBefore(" | ")
+ .substringAfter("Android Studio")
+ .trim()
+ .replace(" ", "")
+
+ if (releaseAnimal.isEmpty() || releaseAnimal.any { it.isDigit() }) return null
+
+ // We only have stable and canary splash screens. Betas use the stable ones.
+ val channel = release.channel.lowercase()
+ .let {
+ when (it) {
+ "release", "rc", "stable", "beta", "patch" -> "stable"
+ "canary", "preview", "alpha" -> "canary"
+ else -> {
+ println(" Note: channel '${it}' isn't supported for splash screens")
+ null
+ }
+ }
+ } ?: return null
+
+ val splashPath = "/studio-splash-screens/$releaseAnimal-$channel.png"
+ val splashFiles = resourceDirs.map { dir -> File(dir, splashPath) }
+ if (splashFiles.none { it.isFile }) {
+ println(" Note: expected splash screen file doesn't exist: '${splashPath}'")
+ return null
+ }
+
+ return "\"$splashPath\""
+ }
+
+ // This is the laziest crap ever, I am sorry.
+ private fun translateDate(rawDate: String): String {
+ val month = rawDate.substringBefore(" ").trimStart('0')
+ val year = rawDate.substringAfterLast(" ".trimStart('0'))
+ val day = rawDate.substring(month.length + 1, rawDate.length - year.length - 1).trimStart('0')
+
+ if (day.isEmpty()) {
+ println("$rawDate\nMonth: '$month'\nYear: '$year'")
+ }
+
+ val monthNumber = when (month.trim().lowercase()) {
+ "january" -> 1
+ "february" -> 2
+ "march" -> 3
+ "april" -> 4
+ "may" -> 5
+ "june" -> 6
+ "july" -> 7
+ "august" -> 8
+ "september" -> 9
+ "october" -> 10
+ "november" -> 11
+ "december" -> 12
+ else -> error("Unrecognized month: $month")
+ }
+
+ return "$year, $monthNumber, $day"
+ }
+
+ private fun readChannel(rawChannel: String) =
+ when (rawChannel.lowercase().trim()) {
+ "stable", "patch", "release" -> "ReleaseChannel.Stable"
+ "beta" -> "ReleaseChannel.Beta"
+ "canary" -> "ReleaseChannel.Canary"
+ else -> "ReleaseChannel.Other"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/ApiAndroidStudioReleases.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/ApiAndroidStudioReleases.kt
new file mode 100644
index 000000000..9fc61f19b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/ApiAndroidStudioReleases.kt
@@ -0,0 +1,51 @@
+package org.jetbrains.jewel.buildlogic.demodata
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ApiAndroidStudioReleases(
+ @SerialName("content")
+ val content: Content = Content()
+) {
+
+ @Serializable
+ internal data class Content(
+ @SerialName("item")
+ val item: List- = listOf(),
+ @SerialName("version")
+ val version: Int = 0
+ ) {
+
+ @Serializable
+ internal data class Item(
+ @SerialName("build")
+ val build: String,
+ @SerialName("channel")
+ val channel: String,
+ @SerialName("date")
+ val date: String,
+ @SerialName("download")
+ val download: List = listOf(),
+ @SerialName("name")
+ val name: String,
+ @SerialName("platformBuild")
+ val platformBuild: String,
+ @SerialName("platformVersion")
+ val platformVersion: String,
+ @SerialName("version")
+ val version: String
+ ) {
+
+ @Serializable
+ internal data class Download(
+ @SerialName("checksum")
+ val checksum: String,
+ @SerialName("link")
+ val link: String,
+ @SerialName("size")
+ val size: String
+ )
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt
index 716d38ed5..829fe4634 100644
--- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt
@@ -30,7 +30,7 @@ internal object IntUiThemeDescriptorReader {
) =
FileSpec.builder(className).apply {
indent(" ")
- this.addFileComment("Generated by the Jewel Int UI Palette Generator\n")
+ addFileComment("Generated by the Jewel Int UI Palette Generator\n")
addFileComment("Generated from the IntelliJ Platform version $ideaVersion\n")
addFileComment("Source: $descriptorUrl")
@@ -175,7 +175,12 @@ internal object IntUiThemeDescriptorReader {
}
addProperty(createOverrideStringMapProperty("iconOverrides", iconOverrides))
- addProperty(createOverrideStringMapProperty("selectionColorPalette", theme.iconColorsOnSelection))
+ addProperty(
+ createOverrideStringMapProperty(
+ "selectionColorPalette",
+ theme.iconColorsOnSelection
+ )
+ )
}.build())
addProperty(
@@ -185,11 +190,11 @@ internal object IntUiThemeDescriptorReader {
)
}
- private fun createOverrideStringMapProperty(name: String, values: Map) =
+ private inline fun createOverrideStringMapProperty(name: String, values: Map) =
PropertySpec.builder(
name = name,
type = Map::class.asTypeName()
- .parameterizedBy(String::class.asTypeName(), String::class.asTypeName()),
+ .parameterizedBy(K::class.asTypeName(), V::class.asTypeName()),
KModifier.OVERRIDE
)
.initializer(
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt
index 0512eeb50..a77427a20 100644
--- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt
@@ -122,5 +122,5 @@ data class IntellijThemeDescriptor(
val colors: Map = emptyMap(),
val ui: Map = emptyMap(),
val icons: Map = emptyMap(),
- val iconColorsOnSelection: Map = emptyMap(),
+ val iconColorsOnSelection: Map = emptyMap(),
)
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 2371ab585..f0a74b272 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.compose.ComposeBuildConfig
+
plugins {
jewel
`jewel-publish`
@@ -5,6 +7,8 @@ plugins {
alias(libs.plugins.kotlinSerialization)
}
+private val composeVersion get() = ComposeBuildConfig.composeVersion
+
dependencies {
- api(compose.desktop.currentOs)
+ api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Button.kt b/core/src/main/kotlin/org/jetbrains/jewel/Button.kt
index 6e7f911ae..7521dda44 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/Button.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/Button.kt
@@ -43,7 +43,7 @@ fun DefaultButton(
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: ButtonStyle = IntelliJTheme.defaultButtonStyle,
- textStyle: TextStyle = IntelliJTheme.defaultTextStyle,
+ textStyle: TextStyle = IntelliJTheme.textStyle,
content: @Composable RowScope.() -> Unit,
) {
ButtonImpl(
@@ -64,7 +64,7 @@ fun OutlinedButton(
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: ButtonStyle = IntelliJTheme.outlinedButtonStyle,
- textStyle: TextStyle = IntelliJTheme.defaultTextStyle,
+ textStyle: TextStyle = IntelliJTheme.textStyle,
content: @Composable RowScope.() -> Unit,
) {
ButtonImpl(
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Checkbox.kt b/core/src/main/kotlin/org/jetbrains/jewel/Checkbox.kt
index cc6d70385..2aaed34b7 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/Checkbox.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/Checkbox.kt
@@ -315,7 +315,7 @@ private fun CheckboxImpl(
val contentColor by colors.contentFor(checkboxState)
CompositionLocalProvider(
LocalTextStyle provides textStyle.copy(color = contentColor.takeOrElse { textStyle.color }),
- LocalContentColor provides contentColor.takeOrElse { textStyle.color },
+ LocalContentColor provides contentColor.takeOrElse { LocalContentColor.current },
) {
content()
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/CircularProgressIndicator.kt b/core/src/main/kotlin/org/jetbrains/jewel/CircularProgressIndicator.kt
new file mode 100644
index 000000000..b605e96af
--- /dev/null
+++ b/core/src/main/kotlin/org/jetbrains/jewel/CircularProgressIndicator.kt
@@ -0,0 +1,170 @@
+package org.jetbrains.jewel
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.takeOrElse
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import org.jetbrains.jewel.styling.CircularProgressStyle
+import org.jetbrains.jewel.util.toHexString
+
+@Composable
+fun CircularProgressIndicator(
+ svgLoader: SvgLoader,
+ modifier: Modifier = Modifier,
+ style: CircularProgressStyle = IntelliJTheme.circularProgressStyle,
+) {
+ CircularProgressIndicatorImpl(
+ modifier = modifier,
+ svgLoader = svgLoader,
+ iconSize = DpSize(16.dp, 16.dp),
+ style = style,
+ frameRetriever = { color -> SpinnerProgressIconGenerator.Small.generateSvgFrames(color.toHexString()) },
+ )
+}
+
+@Composable
+fun CircularProgressIndicatorBig(
+ svgLoader: SvgLoader,
+ modifier: Modifier = Modifier,
+ style: CircularProgressStyle = IntelliJTheme.circularProgressStyle,
+) {
+ CircularProgressIndicatorImpl(
+ modifier = modifier,
+ svgLoader = svgLoader,
+ iconSize = DpSize(32.dp, 32.dp),
+ style = style,
+ frameRetriever = { color -> SpinnerProgressIconGenerator.Big.generateSvgFrames(color.toHexString()) },
+ )
+}
+
+@Composable
+private fun CircularProgressIndicatorImpl(
+ modifier: Modifier = Modifier,
+ svgLoader: SvgLoader,
+ iconSize: DpSize,
+ style: CircularProgressStyle,
+ frameRetriever: (Color) -> List,
+) {
+ val defaultColor = if (IntelliJTheme.isDark) Color(0xFF6F737A) else Color(0xFFA8ADBD)
+ var isFrameReady by remember { mutableStateOf(false) }
+ var currentFrame: Pair by remember { mutableStateOf("" to 0) }
+
+ if (!isFrameReady) {
+ Box(modifier.size(iconSize))
+ } else {
+ Icon(
+ modifier = modifier.size(iconSize),
+ painter = svgLoader.loadRawSvg(
+ currentFrame.first,
+ "circularProgressIndicator_frame_${currentFrame.second}",
+ ),
+ contentDescription = null,
+ )
+ }
+
+ LaunchedEffect(style.color) {
+ val frames = frameRetriever(style.color.takeOrElse { defaultColor })
+ while (true) {
+ for (i in frames.indices) {
+ currentFrame = frames[i] to i
+ isFrameReady = true
+ delay(style.frameTime.inWholeMilliseconds)
+ }
+ }
+ }
+}
+
+object SpinnerProgressIconGenerator {
+
+ private val opacityList = listOf(1.0f, 0.93f, 0.78f, 0.69f, 0.62f, 0.48f, 0.38f, 0.0f)
+
+ private fun StringBuilder.closeRoot() = append("")
+ private fun StringBuilder.openRoot(sizePx: Int) = append(
+ "")
+ private fun StringBuilder.openTag(sizePx: Int) = append(
+ "