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 generateSvgIcon( + size: Int, + opacityListShifted: List, + colorHex: String, + ) = + buildString { + openRoot(size) + elements( + colorHex = colorHex, + opacityList = opacityListShifted, + ) + closeRoot() + } + + private fun StringBuilder.elements( + colorHex: String, + opacityList: List, + ) { + append( + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n", + ) + } + + object Small { + + fun generateSvgFrames(colorHex: String) = buildList { + val opacityListShifted = opacityList.toMutableList() + repeat(opacityList.count()) { + add( + generateSvgIcon( + size = 16, + colorHex = colorHex, + opacityListShifted = opacityListShifted, + ), + ) + opacityListShifted.shtr() + } + } + } + + object Big { + + fun generateSvgFrames(colorHex: String) = buildList { + val opacityListShifted = opacityList.toMutableList() + repeat(opacityList.count()) { + add( + generateSvgIcon( + size = 32, + colorHex = colorHex, + opacityListShifted = opacityListShifted, + ), + ) + opacityListShifted.shtr() + } + } + } + + private fun MutableList.shtr() { + add(first()) + removeFirst() + } +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Divider.kt b/core/src/main/kotlin/org/jetbrains/jewel/Divider.kt index 31c360dc8..6450ae92f 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Divider.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Divider.kt @@ -10,32 +10,37 @@ import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.takeOrElse +import org.jetbrains.jewel.styling.DividerStyle @Composable fun Divider( + orientation: Orientation, modifier: Modifier = Modifier, - color: Color = IntelliJTheme.globalColors.borders.normal, - thickness: Dp = 1.dp, - orientation: Orientation = Orientation.Horizontal, - startIndent: Dp = 0.dp, + color: Color = Color.Unspecified, + thickness: Dp = Dp.Unspecified, + startIndent: Dp = Dp.Unspecified, + style: DividerStyle = IntelliJTheme.dividerStyle, ) { val indentMod = if (startIndent.value != 0f) { - Modifier.padding(start = startIndent) + Modifier.padding(start = startIndent.takeOrElse { style.metrics.startIndent }) } else { Modifier } + val actualThickness = thickness.takeOrElse { style.metrics.thickness } val orientationModifier = when (orientation) { - Orientation.Horizontal -> Modifier.height(thickness).fillMaxWidth() - Orientation.Vertical -> Modifier.width(thickness).fillMaxHeight() + Orientation.Horizontal -> Modifier.height(actualThickness).fillMaxWidth() + Orientation.Vertical -> Modifier.width(actualThickness).fillMaxHeight() } + val lineColor = color.takeOrElse { style.color } Box( modifier .then(indentMod) .then(orientationModifier) - .background(color = color), + .background(color = lineColor), ) } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt b/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt index 971fec0a6..d9c5dbf7b 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt @@ -6,13 +6,14 @@ import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -57,7 +58,7 @@ fun Dropdown( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: DropdownStyle = IntelliJTheme.dropdownStyle, menuContent: MenuScope.() -> Unit, - content: @Composable RowScope.() -> Unit, + content: @Composable BoxScope.() -> Unit, ) { Box { var expanded by remember { mutableStateOf(false) } @@ -116,20 +117,20 @@ fun Dropdown( .border(Stroke.Alignment.Center, style.metrics.borderWidth, borderColor, shape) .appendIf(outline == Outline.None) { focusOutline(outlineState, shape) } .outline(outlineState, outline, shape) + .width(IntrinsicSize.Max) .defaultMinSize(minSize.width, minSize.height.coerceAtLeast(arrowMinSize.height)), contentAlignment = Alignment.CenterStart, ) { CompositionLocalProvider( LocalContentColor provides colors.contentFor(dropdownState).value, ) { - Row( - modifier = Modifier.padding(style.metrics.contentPadding) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(style.metrics.contentPadding) .padding(end = minSize.height), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - content = { - content() - }, + contentAlignment = Alignment.CenterStart, + content = content, ) Box( @@ -148,7 +149,7 @@ fun Dropdown( } if (expanded) { - DropdownMenu( + PopupMenu( onDismissRequest = { expanded = false if (it == InputMode.Touch && dropdownState.isHovered) { @@ -184,8 +185,8 @@ internal fun DropdownMenu( density = density, ) - var focusManager: FocusManager? by mutableStateOf(null) - var inputModeManager: InputModeManager? by mutableStateOf(null) + var focusManager: FocusManager? by remember { mutableStateOf(null) } + var inputModeManager: InputModeManager? by remember { mutableStateOf(null) } val menuManager = remember(onDismissRequest) { MenuManager(onDismissRequest = onDismissRequest) } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/GlobalMetrics.kt b/core/src/main/kotlin/org/jetbrains/jewel/GlobalMetrics.kt index 684ae7a55..a0e11afab 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/GlobalMetrics.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/GlobalMetrics.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.unit.Dp interface GlobalMetrics { val outlineWidth: Dp + val rowHeight: Dp } val LocalGlobalMetrics = staticCompositionLocalOf { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/GroupHeader.kt b/core/src/main/kotlin/org/jetbrains/jewel/GroupHeader.kt index cd3486e5a..a125b34c0 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/GroupHeader.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/GroupHeader.kt @@ -2,9 +2,9 @@ package org.jetbrains.jewel import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import org.jetbrains.jewel.styling.GroupHeaderStyle import org.jetbrains.jewel.styling.LocalGroupHeaderStyle @@ -12,19 +12,16 @@ import org.jetbrains.jewel.styling.LocalGroupHeaderStyle fun GroupHeader( text: String, modifier: Modifier = Modifier, + textColor: Color = Color.Unspecified, style: GroupHeaderStyle = LocalGroupHeaderStyle.current, ) { - CompositionLocalProvider( - LocalContentColor provides style.colors.content, - ) { - Row(modifier, verticalAlignment = Alignment.CenterVertically) { - Text(text) - Divider( - color = style.colors.divider, - orientation = Orientation.Horizontal, - startIndent = style.metrics.indent, - thickness = style.metrics.dividerThickness, - ) - } + Row(modifier, verticalAlignment = Alignment.CenterVertically) { + Text(text, color = textColor) + Divider( + orientation = Orientation.Horizontal, + color = style.colors.divider, + thickness = style.metrics.dividerThickness, + startIndent = style.metrics.indent, + ) } } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Icon.kt b/core/src/main/kotlin/org/jetbrains/jewel/Icon.kt index 8208ab47d..05658f0d6 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Icon.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Icon.kt @@ -90,22 +90,6 @@ fun Icon( ) } -@Composable -fun Icon( - resource: String, - contentDescription: String?, - modifier: Modifier = Modifier, - resourceLoader: ResourceLoader = LocalResourceLoader.current, - tint: Color = Color.Unspecified, -) { - Icon( - painter = painterResource(resource, resourceLoader), - contentDescription = contentDescription, - modifier = modifier, - tint = tint, - ) -} - /** * Icon component that draws a [painter] using [tint], defaulting to * [Color.Unspecified] diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt b/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt new file mode 100644 index 000000000..61b4e4a2f --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt @@ -0,0 +1,80 @@ +package org.jetbrains.jewel + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import org.jetbrains.jewel.styling.IconButtonStyle + +@Composable +fun IconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + style: IconButtonStyle = IntelliJTheme.iconButtonStyle, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable (BoxScope.(ButtonState) -> Unit), +) { + var buttonState by remember(interactionSource) { + mutableStateOf(ButtonState.of(enabled = enabled)) + } + + remember(enabled) { + buttonState = buttonState.copy(enabled = enabled) + } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> buttonState = buttonState.copy(pressed = true) + is PressInteraction.Cancel, is PressInteraction.Release -> + buttonState = + buttonState.copy(pressed = false) + + is HoverInteraction.Enter -> buttonState = buttonState.copy(hovered = true) + is HoverInteraction.Exit -> buttonState = buttonState.copy(hovered = false) + + is FocusInteraction.Focus -> buttonState = buttonState.copy(focused = true) + is FocusInteraction.Unfocus -> buttonState = buttonState.copy(focused = false) + } + } + } + val shape = RoundedCornerShape(style.metrics.cornerSize) + val background by style.colors.backgroundFor(buttonState) + val border by style.colors.borderFor(buttonState) + Box( + modifier = modifier + .defaultMinSize(style.metrics.minSize.width, style.metrics.minSize.height) + .clickable( + onClick = onClick, + enabled = enabled, + role = Role.Button, + interactionSource = interactionSource, + indication = NoIndication, + ) + .padding(style.metrics.padding) + .background(background, shape) + .border(style.metrics.borderWidth, border, shape), + contentAlignment = Alignment.Center, + content = { + content(buttonState) + }, + ) +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IconMapper.kt b/core/src/main/kotlin/org/jetbrains/jewel/IconMapper.kt new file mode 100644 index 000000000..3cc7ee849 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/IconMapper.kt @@ -0,0 +1,28 @@ +package org.jetbrains.jewel + +import androidx.compose.ui.res.ResourceLoader + +interface IconMapper { + + fun mapPath(originalPath: String, iconData: IntelliJThemeIconData, resourceLoader: ResourceLoader): String +} + +object IntelliJIconMapper : IconMapper { + + private const val VERBOSE = false + + override fun mapPath( + originalPath: String, + iconData: IntelliJThemeIconData, + resourceLoader: ResourceLoader, + ): String { + val normalized = "/${originalPath.trimStart('/')}" + val overriddenPath = iconData.iconOverrides[normalized] ?: normalized + + if (overriddenPath != normalized && VERBOSE) { + println("Found theme icon override: '$originalPath' -> '$overriddenPath'") + } + + return overriddenPath + } +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJComponentStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJComponentStyling.kt index ac4cbde9d..53b478cc7 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJComponentStyling.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJComponentStyling.kt @@ -4,9 +4,12 @@ import androidx.compose.runtime.Stable import org.jetbrains.jewel.styling.ButtonStyle import org.jetbrains.jewel.styling.CheckboxStyle import org.jetbrains.jewel.styling.ChipStyle +import org.jetbrains.jewel.styling.CircularProgressStyle +import org.jetbrains.jewel.styling.DividerStyle import org.jetbrains.jewel.styling.DropdownStyle import org.jetbrains.jewel.styling.GroupHeaderStyle import org.jetbrains.jewel.styling.HorizontalProgressBarStyle +import org.jetbrains.jewel.styling.IconButtonStyle import org.jetbrains.jewel.styling.LabelledTextFieldStyle import org.jetbrains.jewel.styling.LazyTreeStyle import org.jetbrains.jewel.styling.LinkStyle @@ -16,6 +19,7 @@ import org.jetbrains.jewel.styling.ScrollbarStyle import org.jetbrains.jewel.styling.TabStyle import org.jetbrains.jewel.styling.TextAreaStyle import org.jetbrains.jewel.styling.TextFieldStyle +import org.jetbrains.jewel.styling.TooltipStyle @Stable class IntelliJComponentStyling( @@ -23,6 +27,7 @@ class IntelliJComponentStyling( val chipStyle: ChipStyle, val defaultButtonStyle: ButtonStyle, val defaultTabStyle: TabStyle, + val dividerStyle: DividerStyle, val dropdownStyle: DropdownStyle, val editorTabStyle: TabStyle, val groupHeaderStyle: GroupHeaderStyle, @@ -36,6 +41,9 @@ class IntelliJComponentStyling( val scrollbarStyle: ScrollbarStyle, val textAreaStyle: TextAreaStyle, val textFieldStyle: TextFieldStyle, + val circularProgressStyle: CircularProgressStyle, + val tooltipStyle: TooltipStyle, + val iconButtonStyle: IconButtonStyle, ) { override fun equals(other: Any?): Boolean { @@ -44,54 +52,63 @@ class IntelliJComponentStyling( other as IntelliJComponentStyling - if (defaultButtonStyle != other.defaultButtonStyle) return false - if (outlinedButtonStyle != other.outlinedButtonStyle) return false if (checkboxStyle != other.checkboxStyle) return false if (chipStyle != other.chipStyle) return false + if (defaultButtonStyle != other.defaultButtonStyle) return false + if (defaultTabStyle != other.defaultTabStyle) return false + if (dividerStyle != other.dividerStyle) return false if (dropdownStyle != other.dropdownStyle) return false + if (editorTabStyle != other.editorTabStyle) return false if (groupHeaderStyle != other.groupHeaderStyle) return false + if (horizontalProgressBarStyle != other.horizontalProgressBarStyle) return false if (labelledTextFieldStyle != other.labelledTextFieldStyle) return false + if (lazyTreeStyle != other.lazyTreeStyle) return false if (linkStyle != other.linkStyle) return false if (menuStyle != other.menuStyle) return false - if (horizontalProgressBarStyle != other.horizontalProgressBarStyle) return false + if (outlinedButtonStyle != other.outlinedButtonStyle) return false if (radioButtonStyle != other.radioButtonStyle) return false if (scrollbarStyle != other.scrollbarStyle) return false if (textAreaStyle != other.textAreaStyle) return false if (textFieldStyle != other.textFieldStyle) return false - if (lazyTreeStyle != other.lazyTreeStyle) return false - if (defaultTabStyle != other.defaultTabStyle) return false - if (editorTabStyle != other.editorTabStyle) return false + if (circularProgressStyle != other.circularProgressStyle) return false + if (tooltipStyle != other.tooltipStyle) return false + if (iconButtonStyle != other.iconButtonStyle) return false return true } override fun hashCode(): Int { - var result = defaultButtonStyle.hashCode() - result = 31 * result + outlinedButtonStyle.hashCode() - result = 31 * result + checkboxStyle.hashCode() + var result = checkboxStyle.hashCode() result = 31 * result + chipStyle.hashCode() + result = 31 * result + defaultButtonStyle.hashCode() + result = 31 * result + defaultTabStyle.hashCode() + result = 31 * result + dividerStyle.hashCode() result = 31 * result + dropdownStyle.hashCode() + result = 31 * result + editorTabStyle.hashCode() result = 31 * result + groupHeaderStyle.hashCode() + result = 31 * result + horizontalProgressBarStyle.hashCode() result = 31 * result + labelledTextFieldStyle.hashCode() + result = 31 * result + lazyTreeStyle.hashCode() result = 31 * result + linkStyle.hashCode() result = 31 * result + menuStyle.hashCode() - result = 31 * result + horizontalProgressBarStyle.hashCode() + result = 31 * result + outlinedButtonStyle.hashCode() result = 31 * result + radioButtonStyle.hashCode() result = 31 * result + scrollbarStyle.hashCode() result = 31 * result + textAreaStyle.hashCode() result = 31 * result + textFieldStyle.hashCode() - result = 31 * result + lazyTreeStyle.hashCode() - result = 31 * result + defaultTabStyle.hashCode() - result = 31 * result + editorTabStyle.hashCode() + result = 31 * result + circularProgressStyle.hashCode() + result = 31 * result + tooltipStyle.hashCode() + result = 31 * result + iconButtonStyle.hashCode() return result } - override fun toString(): String = + override fun toString() = "IntelliJComponentStyling(checkboxStyle=$checkboxStyle, chipStyle=$chipStyle, " + - "defaultButtonStyle=$defaultButtonStyle, defaultTabStyle=$defaultTabStyle, dropdownStyle=$dropdownStyle, " + - "editorTabStyle=$editorTabStyle, groupHeaderStyle=$groupHeaderStyle, " + - "horizontalProgressBarStyle=$horizontalProgressBarStyle, labelledTextFieldStyle=$labelledTextFieldStyle, " + - "lazyTreeStyle=$lazyTreeStyle, linkStyle=$linkStyle, menuStyle=$menuStyle, " + - "outlinedButtonStyle=$outlinedButtonStyle, radioButtonStyle=$radioButtonStyle, " + - "scrollbarStyle=$scrollbarStyle, textAreaStyle=$textAreaStyle, textFieldStyle=$textFieldStyle)" + "defaultButtonStyle=$defaultButtonStyle, defaultTabStyle=$defaultTabStyle, " + + "dividerStyle=$dividerStyle, dropdownStyle=$dropdownStyle, editorTabStyle=$editorTabStyle, " + + "groupHeaderStyle=$groupHeaderStyle, horizontalProgressBarStyle=$horizontalProgressBarStyle, " + + "labelledTextFieldStyle=$labelledTextFieldStyle, lazyTreeStyle=$lazyTreeStyle, linkStyle=$linkStyle, " + + "menuStyle=$menuStyle, outlinedButtonStyle=$outlinedButtonStyle, radioButtonStyle=$radioButtonStyle, " + + "scrollbarStyle=$scrollbarStyle, textAreaStyle=$textAreaStyle, textFieldStyle=$textFieldStyle, " + + "circularProgressStyle=$circularProgressStyle, tooltipStyle=$tooltipStyle, iconButtonStyle=$iconButtonStyle)" } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt index 6c26b6eac..9b5472886 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt @@ -9,20 +9,26 @@ import androidx.compose.ui.text.TextStyle import org.jetbrains.jewel.styling.ButtonStyle import org.jetbrains.jewel.styling.CheckboxStyle import org.jetbrains.jewel.styling.ChipStyle +import org.jetbrains.jewel.styling.CircularProgressStyle +import org.jetbrains.jewel.styling.DividerStyle import org.jetbrains.jewel.styling.DropdownStyle import org.jetbrains.jewel.styling.GroupHeaderStyle import org.jetbrains.jewel.styling.HorizontalProgressBarStyle +import org.jetbrains.jewel.styling.IconButtonStyle import org.jetbrains.jewel.styling.LabelledTextFieldStyle import org.jetbrains.jewel.styling.LazyTreeStyle import org.jetbrains.jewel.styling.LinkStyle import org.jetbrains.jewel.styling.LocalCheckboxStyle import org.jetbrains.jewel.styling.LocalChipStyle +import org.jetbrains.jewel.styling.LocalCircularProgressStyle import org.jetbrains.jewel.styling.LocalDefaultButtonStyle import org.jetbrains.jewel.styling.LocalDefaultTabStyle +import org.jetbrains.jewel.styling.LocalDividerStyle import org.jetbrains.jewel.styling.LocalDropdownStyle import org.jetbrains.jewel.styling.LocalEditorTabStyle import org.jetbrains.jewel.styling.LocalGroupHeaderStyle import org.jetbrains.jewel.styling.LocalHorizontalProgressBarStyle +import org.jetbrains.jewel.styling.LocalIconButtonStyle import org.jetbrains.jewel.styling.LocalLabelledTextFieldStyle import org.jetbrains.jewel.styling.LocalLazyTreeStyle import org.jetbrains.jewel.styling.LocalLinkStyle @@ -32,12 +38,14 @@ import org.jetbrains.jewel.styling.LocalRadioButtonStyle import org.jetbrains.jewel.styling.LocalScrollbarStyle import org.jetbrains.jewel.styling.LocalTextAreaStyle import org.jetbrains.jewel.styling.LocalTextFieldStyle +import org.jetbrains.jewel.styling.LocalTooltipStyle import org.jetbrains.jewel.styling.MenuStyle import org.jetbrains.jewel.styling.RadioButtonStyle import org.jetbrains.jewel.styling.ScrollbarStyle import org.jetbrains.jewel.styling.TabStyle import org.jetbrains.jewel.styling.TextAreaStyle import org.jetbrains.jewel.styling.TextFieldStyle +import org.jetbrains.jewel.styling.TooltipStyle interface IntelliJTheme { @@ -57,7 +65,7 @@ interface IntelliJTheme { @ReadOnlyComposable get() = LocalGlobalMetrics.current - val defaultTextStyle: TextStyle + val textStyle: TextStyle @Composable @ReadOnlyComposable get() = LocalTextStyle.current @@ -111,6 +119,11 @@ interface IntelliJTheme { @ReadOnlyComposable get() = LocalChipStyle.current + val dividerStyle: DividerStyle + @Composable + @ReadOnlyComposable + get() = LocalDividerStyle.current + val dropdownStyle: DropdownStyle @Composable @ReadOnlyComposable @@ -175,6 +188,20 @@ interface IntelliJTheme { @Composable @ReadOnlyComposable get() = LocalEditorTabStyle.current + + val circularProgressStyle: CircularProgressStyle + @Composable + @ReadOnlyComposable + get() = LocalCircularProgressStyle.current + + val tooltipStyle: TooltipStyle + @Composable + @ReadOnlyComposable + get() = LocalTooltipStyle.current + val iconButtonStyle: IconButtonStyle + @Composable + @ReadOnlyComposable + get() = LocalIconButtonStyle.current } } @@ -191,12 +218,10 @@ fun IntelliJTheme( @Composable fun IntelliJTheme(theme: IntelliJThemeDefinition, content: @Composable () -> Unit) { - val defaultTextStyle = theme.defaultTextStyle - CompositionLocalProvider( LocalIsDarkTheme provides theme.isDark, - LocalContentColor provides defaultTextStyle.color, - LocalTextStyle provides defaultTextStyle, + LocalContentColor provides theme.contentColor, + LocalTextStyle provides theme.defaultTextStyle, LocalGlobalColors provides theme.globalColors, LocalGlobalMetrics provides theme.globalMetrics, content = content, diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt index 605a6b54f..31ba5d9f4 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle @Immutable @@ -10,6 +11,7 @@ interface IntelliJThemeDefinition { val globalColors: GlobalColors val globalMetrics: GlobalMetrics val defaultTextStyle: TextStyle + val contentColor: Color val colorPalette: IntelliJThemeColorPalette val iconData: IntelliJThemeIconData diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt index 0531c1f61..18a42d88b 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt @@ -7,13 +7,13 @@ import androidx.compose.ui.graphics.Color interface IntelliJThemeIconData { val iconOverrides: Map - val colorPalette: Map - val selectionColorPalette: Map + val colorPalette: Map + val selectionColorPalette: Map fun selectionColorMapping() = selectionColorPalette.mapNotNull { (key, value) -> val keyColor = key.toColorOrNull() ?: return@mapNotNull null - val valueColor = value.toColorOrNull() ?: return@mapNotNull null + val valueColor = Color(value) keyColor to valueColor }.toMap() } @@ -41,7 +41,7 @@ object EmptyThemeIconData : IntelliJThemeIconData { override val colorPalette: Map = emptyMap() - override val selectionColorPalette: Map = emptyMap() + override val selectionColorPalette: Map = emptyMap() override fun toString() = "EmptyThemeIconData(iconOverrides=[], colorPalette=[], selectionColorPalette=[])" diff --git a/core/src/main/kotlin/org/jetbrains/jewel/InternalJewelApi.kt b/core/src/main/kotlin/org/jetbrains/jewel/InternalJewelApi.kt index a5e62faf7..2c91a33a1 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/InternalJewelApi.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/InternalJewelApi.kt @@ -8,5 +8,6 @@ package org.jetbrains.jewel AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.PROPERTY, ) annotation class InternalJewelApi diff --git a/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt index d6a3c3fb6..b0f315471 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt @@ -5,7 +5,7 @@ import java.io.InputStream abstract class JewelResourceLoader : ResourceLoader { - protected var verbose = true + private val verbose = true protected fun loadResourceOrNull(path: String, classLoaders: List): InputStream? { for (classLoader in classLoaders) { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt index e93f4749c..ab1df5d01 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt @@ -14,6 +14,7 @@ import java.util.concurrent.ConcurrentHashMap class JewelSvgLoader(private val svgPatcher: SvgPatcher) : SvgLoader { private val cache = ConcurrentHashMap() + private val rawSvgCache = ConcurrentHashMap() @Composable override fun loadSvgResource( @@ -42,6 +43,27 @@ class JewelSvgLoader(private val svgPatcher: SvgPatcher) : SvgLoader { return remember(resourcePath, density, loader) { painter } } + @Composable + override fun loadRawSvg(rawSvg: String, key: String): Painter = + rawSvg.byteInputStream().use { loadRawSvg(it, key) } + + @Composable + override fun loadRawSvg(rawSvg: InputStream, key: String): Painter { + rawSvgCache[key]?.let { return it } + + val painter = rememberRawSvgResource(rawSvg, key) + cache[key] = painter + return painter + } + + @Composable + private fun rememberRawSvgResource(rawSvg: InputStream, key: String): Painter { + val density = LocalDensity.current + + val painter = loadSvgPainter(rawSvg, density) + return remember(key, density) { painter } + } + private fun InputStream.patchColors(resourcePath: String): InputStream = svgPatcher.patchSvg(this, resourcePath).byteInputStream() diff --git a/core/src/main/kotlin/org/jetbrains/jewel/LabelledTextField.kt b/core/src/main/kotlin/org/jetbrains/jewel/LabelledTextField.kt index 05a1937fd..af2f8fd17 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/LabelledTextField.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/LabelledTextField.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.unit.offset import org.jetbrains.jewel.styling.LabelledTextFieldStyle /** - * @param placeholder the optional placeholder to be displayed over the component when - * the [value] is empty. - * @param hint the optional hint to be displayed underneath the component. By default it - * will have a greyed out appearance and smaller text. * @param label the label to display above the component. + * @param hint the optional hint to be displayed underneath the component. + * By default it will have a greyed out appearance and smaller text. + * @param placeholder the optional placeholder to be displayed over the + * component when the [value] is empty. */ @Composable fun LabelledTextField( @@ -41,13 +41,14 @@ fun LabelledTextField( hint: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, undecorated: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: LabelledTextFieldStyle = IntelliJTheme.labelledTextFieldStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) } @@ -72,6 +73,7 @@ fun LabelledTextField( hint = hint, placeholder = placeholder, trailingIcon = trailingIcon, + leadingIcon = leadingIcon, undecorated = undecorated, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, @@ -84,11 +86,11 @@ fun LabelledTextField( } /** - * @param placeholder the optional placeholder to be displayed over the component when - * the [value] is empty. - * @param hint the optional hint to be displayed underneath the component. By default it - * will have a greyed out appearance and smaller text. * @param label the label to display above the component. + * @param hint the optional hint to be displayed underneath the component. + * By default it will have a greyed out appearance and smaller text. + * @param placeholder the optional placeholder to be displayed over the + * component when the [value] is empty. */ @Composable fun LabelledTextField( @@ -103,24 +105,19 @@ fun LabelledTextField( hint: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, undecorated: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: LabelledTextFieldStyle = IntelliJTheme.labelledTextFieldStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { LabelledTextFieldLayout( modifier = modifier, - label = { - CompositionLocalProvider( - LocalTextStyle provides style.textStyles.label, - LocalContentColor provides style.colors.label, - content = label, - ) - }, + label = label, textField = { TextField( value = value, @@ -131,6 +128,7 @@ fun LabelledTextField( outline = outline, placeholder = placeholder, trailingIcon = trailingIcon, + leadingIcon = leadingIcon, undecorated = undecorated, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Link.kt b/core/src/main/kotlin/org/jetbrains/jewel/Link.kt index fae5deaac..48ee76234 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Link.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Link.kt @@ -176,7 +176,7 @@ fun DropdownLink( ) if (expanded) { - DropdownMenu( + PopupMenu( onDismissRequest = { expanded = false if (it == InputMode.Touch && hovered) { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt b/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt index f016412a7..733cb202d 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt @@ -62,13 +62,70 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import org.jetbrains.jewel.CommonStateBitMask.Active +import org.jetbrains.jewel.CommonStateBitMask.Enabled +import org.jetbrains.jewel.CommonStateBitMask.Focused +import org.jetbrains.jewel.CommonStateBitMask.Hovered +import org.jetbrains.jewel.CommonStateBitMask.Pressed +import org.jetbrains.jewel.CommonStateBitMask.Selected import org.jetbrains.jewel.foundation.Stroke import org.jetbrains.jewel.foundation.border import org.jetbrains.jewel.foundation.onHover +import org.jetbrains.jewel.styling.LocalMenuStyle import org.jetbrains.jewel.styling.MenuItemColors import org.jetbrains.jewel.styling.MenuItemMetrics import org.jetbrains.jewel.styling.MenuStyle +@Composable +fun PopupMenu( + onDismissRequest: (InputMode) -> Boolean, + horizontalAlignment: Alignment.Horizontal, + resourceLoader: ResourceLoader, + modifier: Modifier = Modifier, + style: MenuStyle = IntelliJTheme.menuStyle, + content: MenuScope.() -> Unit, +) { + val density = LocalDensity.current + + val popupPositionProvider = AnchorVerticalMenuPositionProvider( + contentOffset = style.metrics.offset, + contentMargin = style.metrics.menuMargin, + alignment = horizontalAlignment, + density = density, + ) + + var focusManager: FocusManager? by mutableStateOf(null) + var inputModeManager: InputModeManager? by mutableStateOf(null) + val menuManager = remember(onDismissRequest) { + MenuManager(onDismissRequest = onDismissRequest) + } + + Popup( + popupPositionProvider = popupPositionProvider, + onDismissRequest = { onDismissRequest(InputMode.Touch) }, + properties = PopupProperties(focusable = true), + onPreviewKeyEvent = { false }, + onKeyEvent = { + val currentFocusManager = checkNotNull(focusManager) { "FocusManager must not be null" } + val currentInputModeManager = checkNotNull(inputModeManager) { "InputModeManager must not be null" } + handlePopupMenuOnKeyEvent(it, currentFocusManager, currentInputModeManager, menuManager) + }, + ) { + focusManager = LocalFocusManager.current + inputModeManager = LocalInputModeManager.current + + CompositionLocalProvider( + LocalMenuManager provides menuManager, + LocalMenuStyle provides style, + ) { + MenuContent( + modifier = modifier, + content = content, + resourceLoader = resourceLoader, + ) + } + } +} + @Composable internal fun MenuContent( resourceLoader: ResourceLoader, @@ -239,10 +296,10 @@ fun MenuSeparator( colors: MenuItemColors = IntelliJTheme.menuStyle.colors.itemColors, ) { Divider( - modifier = modifier.padding(metrics.separatorPadding), - thickness = metrics.separatorThickness, orientation = Orientation.Horizontal, + modifier = modifier.padding(metrics.separatorPadding), color = colors.separator, + thickness = metrics.separatorThickness, ) } @@ -356,7 +413,7 @@ fun MenuSubmenuItem( is PressInteraction.Press -> itemState = itemState.copy(pressed = true) is PressInteraction.Cancel, is PressInteraction.Release -> itemState = itemState.copy(pressed = false) is HoverInteraction.Enter -> { - itemState = itemState.copy(hovered = true) + itemState = itemState.copy(hovered = true, selected = true) focusRequester.requestFocus() } @@ -479,8 +536,8 @@ internal fun Submenu( density = density, ) - var focusManager: FocusManager? by mutableStateOf(null) - var inputModeManager: InputModeManager? by mutableStateOf(null) + var focusManager: FocusManager? by remember { mutableStateOf(null) } + var inputModeManager: InputModeManager? by remember { mutableStateOf(null) } val parentMenuManager = LocalMenuManager.current val menuManager = remember(parentMenuManager, onDismissRequest) { parentMenuManager.submenuManager(onDismissRequest) @@ -556,12 +613,6 @@ value class MenuItemState(val state: ULong) : SelectableComponentState { companion object { - private val Enabled = 1UL shl 0 - private val Focused = 1UL shl 1 - private val Hovered = 1UL shl 2 - private val Pressed = 1UL shl 3 - private val Selected = 1UL shl 4 - fun of( selected: Boolean, enabled: Boolean, diff --git a/core/src/main/kotlin/org/jetbrains/jewel/NoIndication.kt b/core/src/main/kotlin/org/jetbrains/jewel/NoIndication.kt new file mode 100644 index 000000000..c099ab0cf --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/NoIndication.kt @@ -0,0 +1,21 @@ +package org.jetbrains.jewel + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.drawscope.ContentDrawScope + +object NoIndication : Indication { + + private object NoIndicationInstance : IndicationInstance { + + override fun ContentDrawScope.drawIndication() { + drawContent() + } + } + + @Composable + override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance = + NoIndicationInstance +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/RadioButton.kt b/core/src/main/kotlin/org/jetbrains/jewel/RadioButton.kt index 67e490d52..051ecf91f 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/RadioButton.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/RadioButton.kt @@ -34,6 +34,7 @@ import org.jetbrains.jewel.CommonStateBitMask.Enabled import org.jetbrains.jewel.CommonStateBitMask.Focused import org.jetbrains.jewel.CommonStateBitMask.Hovered import org.jetbrains.jewel.CommonStateBitMask.Pressed +import org.jetbrains.jewel.CommonStateBitMask.Selected import org.jetbrains.jewel.foundation.Stroke import org.jetbrains.jewel.styling.RadioButtonStyle @@ -47,7 +48,7 @@ fun RadioButton( outline: Outline = Outline.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: RadioButtonStyle = IntelliJTheme.radioButtonStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, ) { RadioButtonImpl( selected = selected, @@ -74,7 +75,7 @@ fun RadioButtonRow( outline: Outline = Outline.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: RadioButtonStyle = IntelliJTheme.radioButtonStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, ) { RadioButtonImpl( selected = selected, @@ -101,7 +102,7 @@ fun RadioButtonRow( outline: Outline = Outline.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: RadioButtonStyle = IntelliJTheme.radioButtonStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, content: @Composable RowScope.() -> Unit, ) { RadioButtonImpl( @@ -183,7 +184,7 @@ private fun RadioButtonImpl( val contentColor by colors.contentFor(radioButtonState) CompositionLocalProvider( LocalTextStyle provides textStyle.copy(color = contentColor.takeOrElse { textStyle.color }), - LocalContentColor provides contentColor.takeOrElse { textStyle.color }, + LocalContentColor provides contentColor.takeOrElse { textStyle.color.takeOrElse { LocalContentColor.current } }, ) { content() } @@ -249,10 +250,6 @@ value class RadioButtonState(val state: ULong) : SelectableComponentState { companion object { - private const val SELECTED_BIT_OFFSET = CommonStateBitMask.FIRST_AVAILABLE_OFFSET - - private val Selected = 1UL shl SELECTED_BIT_OFFSET - fun of( selected: Boolean, enabled: Boolean = true, diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Scrollbars.kt b/core/src/main/kotlin/org/jetbrains/jewel/Scrollbars.kt index e6e9b8131..efab635a8 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Scrollbars.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Scrollbars.kt @@ -27,14 +27,14 @@ fun VerticalScrollbar( style: ScrollbarStyle = IntelliJTheme.scrollbarStyle, ) { val shape by remember { mutableStateOf(RoundedCornerShape(style.metrics.thumbCornerSize)) } - val hoverDurationMillis by remember { mutableStateOf(style.hoverDuration.toInt(DurationUnit.MILLISECONDS)) } + val hoverDurationMillis by remember { mutableStateOf(style.hoverDuration.inWholeMilliseconds) } CompositionLocalProvider( LocalScrollbarStyle provides ComposeScrollbarStyle( minimalHeight = style.metrics.minThumbLength, thickness = style.metrics.thumbThickness, shape = shape, - hoverDurationMillis = hoverDurationMillis, + hoverDurationMillis = hoverDurationMillis.toInt(), unhoverColor = style.colors.thumbBackground, hoverColor = style.colors.thumbBackgroundHovered, ), diff --git a/core/src/main/kotlin/org/jetbrains/jewel/SplitLayout.kt b/core/src/main/kotlin/org/jetbrains/jewel/SplitLayout.kt new file mode 100644 index 000000000..f85c57677 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/SplitLayout.kt @@ -0,0 +1,198 @@ +package org.jetbrains.jewel + +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +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.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import java.awt.Cursor +import kotlin.math.roundToInt +import androidx.compose.foundation.gestures.Orientation as ComposeOrientation + +@Composable +fun HorizontalSplitLayout( + first: @Composable (Modifier) -> Unit, + second: @Composable (Modifier) -> Unit, + modifier: Modifier = Modifier, + dividerColor: Color = IntelliJTheme.globalColors.borders.normal, + dividerThickness: Dp = 1.dp, + dividerIndent: Dp = 0.dp, + draggableWidth: Dp = 8.dp, + minRatio: Float = 0f, + maxRatio: Float = 1f, + initialDividerPosition: Dp = 300.dp, +) { + val density = LocalDensity.current + var dividerX by remember { + mutableStateOf(with(density) { initialDividerPosition.roundToPx() }) + } + + Layout(modifier = modifier, content = { + val dividerInteractionSource = remember { MutableInteractionSource() } + first(Modifier.layoutId("first")) + + Divider( + orientation = Orientation.Vertical, + modifier = Modifier.fillMaxHeight().layoutId("divider"), + color = dividerColor, + thickness = dividerThickness, + startIndent = dividerIndent, + ) + + second(Modifier.layoutId("second")) + + Box( + Modifier.fillMaxHeight() + .width(draggableWidth) + .draggable( + interactionSource = dividerInteractionSource, + orientation = ComposeOrientation.Horizontal, + state = rememberDraggableState { delta -> + dividerX += delta.toInt() + }, + ) + .pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) + .layoutId("divider-handle"), + ) + }) { measurables, incomingConstraints -> + val availableWidth = incomingConstraints.maxWidth + val actualDividerX = dividerX.coerceIn(0, availableWidth) + .coerceIn((availableWidth * minRatio).roundToInt(), (availableWidth * maxRatio).roundToInt()) + + val dividerMeasurable = measurables.single { it.layoutId == "divider" } + val dividerPlaceable = dividerMeasurable + .measure(Constraints.fixed(dividerThickness.roundToPx(), incomingConstraints.maxHeight)) + + val firstComponentConstraints = + Constraints.fixed((actualDividerX).coerceAtLeast(0), incomingConstraints.maxHeight) + val firstPlaceable = ( + measurables.find { it.layoutId == "first" } + ?: error("No first component found. Have you applied the provided Modifier to it?") + ) + .measure(firstComponentConstraints) + + val secondComponentConstraints = + Constraints.fixed( + availableWidth - actualDividerX + dividerPlaceable.width, + incomingConstraints.maxHeight, + ) + val secondPlaceable = ( + measurables.find { it.layoutId == "second" } + ?: error("No second component found. Have you applied the provided Modifier to it?") + ) + .measure(secondComponentConstraints) + + val dividerHandlePlaceable = measurables.single { it.layoutId == "divider-handle" } + .measure(Constraints.fixedHeight(incomingConstraints.maxHeight)) + + layout(availableWidth, incomingConstraints.maxHeight) { + firstPlaceable.placeRelative(0, 0) + dividerPlaceable.placeRelative(actualDividerX - dividerPlaceable.width / 2, 0) + secondPlaceable.placeRelative(actualDividerX + dividerPlaceable.width, 0) + dividerHandlePlaceable.placeRelative(actualDividerX - dividerHandlePlaceable.measuredWidth / 2, 0) + } + } +} + +@Composable +fun VerticalSplitLayout( + first: @Composable (Modifier) -> Unit, + second: @Composable (Modifier) -> Unit, + modifier: Modifier = Modifier, + dividerColor: Color = IntelliJTheme.globalColors.borders.normal, + dividerThickness: Dp = 1.dp, + dividerIndent: Dp = 0.dp, + draggableWidth: Dp = 8.dp, + minRatio: Float = 0f, + maxRatio: Float = 1f, + initialDividerPosition: Dp = 300.dp, +) { + val density = LocalDensity.current + var dividerY by remember { + mutableStateOf(with(density) { initialDividerPosition.roundToPx() }) + } + + Layout(modifier = modifier, content = { + val dividerInteractionSource = remember { MutableInteractionSource() } + first(Modifier.layoutId("first")) + + Divider( + orientation = Orientation.Horizontal, + modifier = Modifier.fillMaxHeight().layoutId("divider"), + color = dividerColor, + thickness = dividerThickness, + startIndent = dividerIndent, + ) + + second(Modifier.layoutId("second")) + + Box( + Modifier.fillMaxWidth() + .height(draggableWidth) + .draggable( + interactionSource = dividerInteractionSource, + orientation = ComposeOrientation.Vertical, + state = rememberDraggableState { delta -> + dividerY += delta.toInt() + }, + ) + .pointerHoverIcon(PointerIcon(Cursor(Cursor.N_RESIZE_CURSOR))) + .layoutId("divider-handle"), + ) + }) { measurables, incomingConstraints -> + val availableHeight = incomingConstraints.maxHeight + val actualDividerY = dividerY.coerceIn(0, availableHeight) + .coerceIn((availableHeight * minRatio).roundToInt(), (availableHeight * maxRatio).roundToInt()) + + val dividerMeasurable = measurables.single { it.layoutId == "divider" } + val dividerPlaceable = dividerMeasurable + .measure(Constraints.fixed(incomingConstraints.maxWidth, dividerThickness.roundToPx())) + + val firstComponentConstraints = + Constraints.fixed(incomingConstraints.maxWidth, (actualDividerY - 1).coerceAtLeast(0)) + val firstPlaceable = ( + measurables.find { it.layoutId == "first" } + ?: error("No first component found. Have you applied the provided Modifier to it?") + ) + .measure(firstComponentConstraints) + + val secondComponentConstraints = + Constraints.fixed( + incomingConstraints.maxWidth, + availableHeight - actualDividerY + dividerPlaceable.height, + ) + val secondPlaceable = ( + measurables.find { it.layoutId == "second" } + ?: error("No second component found. Have you applied the provided Modifier to it?") + ) + .measure(secondComponentConstraints) + + val dividerHandlePlaceable = measurables.single { it.layoutId == "divider-handle" } + .measure(Constraints.fixedWidth(incomingConstraints.maxWidth)) + + layout(incomingConstraints.maxWidth, availableHeight) { + firstPlaceable.placeRelative(0, 0) + dividerPlaceable.placeRelative(0, actualDividerY - dividerPlaceable.height / 2) + secondPlaceable.placeRelative(0, actualDividerY + dividerPlaceable.height) + dividerHandlePlaceable.placeRelative(0, actualDividerY - dividerHandlePlaceable.measuredHeight / 2) + } + } +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt index 1653c8668..a2b42d81c 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt @@ -3,13 +3,56 @@ package org.jetbrains.jewel import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.ResourceLoader +import java.io.InputStream interface SvgLoader { + /** + * Creates a [Painter] from the provided [svgPath], using the + * [resourceLoader] and [pathPatcher] to locate the correct resource + * file. The icon colors are patched, if needed, and the result is + * cached in memory. + * + * @param svgPath The path to the SVG resource to load. + * @param resourceLoader The [ResourceLoader] to use to load the resource. + * @param pathPatcher A function that can be used to patch the path of + * the resource (e.g., for mapping to New UI icons in the IJ Platform). + */ @Composable fun loadSvgResource( svgPath: String, resourceLoader: ResourceLoader, pathPatcher: @Composable (String) -> String, ): Painter + + /** + * Creates a [Painter] from the provided [rawSvg], using the [key] + * to maintain an in-memory cache of the loaded SVG. + * + * The [rawSvg] stream is **not** automatically closed after being + * consumed. + * + * Note: when loading raw SVGs, icon color patching is not possible. + * The SVG contents are not manipulated in any way before loading. + * + * @param rawSvg An [InputStream] containing a raw SVG. + * @param key A unique name for the SVG that is being loaded, used + * for in-memory caching. + */ + @Composable + fun loadRawSvg(rawSvg: InputStream, key: String): Painter + + /** + * Creates a [Painter] from the provided [rawSvg], using the [key] + * to maintain an in-memory cache of the loaded SVG. + * + * Note: when loading raw SVGs, icon color patching is not possible. + * The SVG contents are not manipulated in any way before loading. + * + * @param rawSvg A [String] containing a raw SVG. + * @param key A unique name for the SVG that is being loaded, used + * for in-memory caching. + */ + @Composable + fun loadRawSvg(rawSvg: String, key: String): Painter } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Tabs.kt b/core/src/main/kotlin/org/jetbrains/jewel/Tabs.kt index f9ed07314..ff36c4697 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Tabs.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Tabs.kt @@ -1,13 +1,10 @@ package org.jetbrains.jewel import androidx.compose.foundation.Image -import androidx.compose.foundation.Indication -import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.HoverInteraction -import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement @@ -33,7 +30,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.isTertiary import androidx.compose.ui.input.pointer.onPointerEvent @@ -81,7 +78,7 @@ internal fun TabImpl( CompositionLocalProvider( LocalIndication provides NoIndication, - LocalContentColor provides tabStyle.colors.contentFor(tabState).value, + LocalContentColor provides tabStyle.colors.contentFor(tabState).value.takeOrElse { LocalContentColor.current }, ) { val labelAlpha by tabStyle.contentAlpha.labelFor(tabState) val iconAlpha by tabStyle.contentAlpha.iconFor(tabState) @@ -165,21 +162,6 @@ internal fun TabImpl( } } -private object NoIndication : Indication { - private object NoIndicationInstance : IndicationInstance { - - override fun ContentDrawScope.drawIndication() { - drawContent() - } - } - - @Suppress("ExpressionBodySyntax") - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { - return NoIndicationInstance - } -} - @Immutable @JvmInline value class TabState(val state: ULong) : SelectableComponentState { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Text.kt b/core/src/main/kotlin/org/jetbrains/jewel/Text.kt index 255b87a6f..467a65642 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Text.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Text.kt @@ -35,7 +35,7 @@ fun Text( softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, - style: TextStyle = IntelliJTheme.defaultTextStyle, + style: TextStyle = IntelliJTheme.textStyle, ) { Text( AnnotatedString(text), @@ -76,13 +76,11 @@ fun Text( maxLines: Int = Int.MAX_VALUE, inlineContent: Map = emptyMap(), onTextLayout: (TextLayoutResult) -> Unit = {}, - style: TextStyle = IntelliJTheme.defaultTextStyle, + style: TextStyle = IntelliJTheme.textStyle, ) { val textColor = color.takeOrElse { style.color.takeOrElse { - LocalContentColor.current.takeOrElse { - LocalTextStyle.current.color - } + LocalContentColor.current } } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt b/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt index 1dbc198e1..8929a96ee 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt @@ -46,7 +46,7 @@ fun TextArea( maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextAreaStyle = IntelliJTheme.textAreaStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) } @@ -102,7 +102,7 @@ fun TextArea( maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextAreaStyle = IntelliJTheme.textAreaStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { val minSize = style.metrics.minSize diff --git a/core/src/main/kotlin/org/jetbrains/jewel/TextField.kt b/core/src/main/kotlin/org/jetbrains/jewel/TextField.kt index f499a8f0e..199967e90 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/TextField.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/TextField.kt @@ -41,6 +41,7 @@ fun TextField( outline: Outline = Outline.None, placeholder: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, undecorated: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, @@ -71,6 +72,7 @@ fun TextField( outline = outline, placeholder = placeholder, trailingIcon = trailingIcon, + leadingIcon = leadingIcon, undecorated = undecorated, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, @@ -94,6 +96,7 @@ fun TextField( readOnly: Boolean = false, outline: Outline = Outline.None, placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, undecorated: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, @@ -101,7 +104,7 @@ fun TextField( keyboardActions: KeyboardActions = KeyboardActions(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextFieldStyle = IntelliJTheme.textFieldStyle, - textStyle: TextStyle = IntelliJTheme.defaultTextStyle, + textStyle: TextStyle = IntelliJTheme.textStyle, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { InputField( @@ -132,6 +135,7 @@ fun TextField( placeholderTextColor = style.colors.placeholder, placeholder = if (value.text.isEmpty()) placeholder else null, trailingIcon = trailingIcon, + leadingIcon = leadingIcon, ) } } @@ -144,10 +148,16 @@ private fun TextFieldDecorationBox( placeholderTextColor: Color, placeholder: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, ) { Layout( modifier = modifier, content = { + if (leadingIcon != null) { + Box(modifier = Modifier.layoutId(LEADING_ID), contentAlignment = Alignment.Center) { + leadingIcon() + } + } if (trailingIcon != null) { Box(modifier = Modifier.layoutId(TRAILING_ID), contentAlignment = Alignment.Center) { trailingIcon() @@ -175,7 +185,11 @@ private fun TextFieldDecorationBox( // measure trailing icon val trailingPlaceable = measurables.find { it.layoutId == TRAILING_ID } ?.measure(iconConstraints) + + val leadingPlaceable = measurables.find { it.layoutId == LEADING_ID } + ?.measure(iconConstraints) occupiedSpaceHorizontally += trailingPlaceable?.width ?: 0 + occupiedSpaceHorizontally += leadingPlaceable?.width ?: 0 val textFieldConstraints = incomingConstraints.offset( horizontal = -occupiedSpaceHorizontally, @@ -188,12 +202,14 @@ private fun TextFieldDecorationBox( val width = calculateWidth( trailingPlaceable, + leadingPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints, ) val height = calculateHeight( trailingPlaceable, + leadingPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints, @@ -204,6 +220,7 @@ private fun TextFieldDecorationBox( height, width, trailingPlaceable, + leadingPlaceable, textFieldPlaceable, placeholderPlaceable, ) @@ -213,6 +230,7 @@ private fun TextFieldDecorationBox( private fun calculateWidth( trailingPlaceable: Placeable?, + leadingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, @@ -221,12 +239,13 @@ private fun calculateWidth( textFieldPlaceable.width, placeholderPlaceable?.width ?: 0, ) - val wrappedWidth = middleSection + (trailingPlaceable?.width ?: 0) + val wrappedWidth = middleSection + (trailingPlaceable?.width ?: 0) + (leadingPlaceable?.width ?: 0) return max(wrappedWidth, constraints.minWidth) } private fun calculateHeight( trailingPlaceable: Placeable?, + leadingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, @@ -234,6 +253,7 @@ private fun calculateHeight( textFieldPlaceable.height, placeholderPlaceable?.height ?: 0, trailingPlaceable?.height ?: 0, + leadingPlaceable?.height ?: 0, constraints.minHeight, ) @@ -241,6 +261,7 @@ private fun Placeable.PlacementScope.place( height: Int, width: Int, trailingPlaceable: Placeable?, + leadingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, ) { @@ -249,16 +270,20 @@ private fun Placeable.PlacementScope.place( width - trailingPlaceable.width, Alignment.CenterVertically.align(trailingPlaceable.height, height), ) + leadingPlaceable?.placeRelative( + 0, + Alignment.CenterVertically.align(leadingPlaceable.height, height), + ) // placed center vertically textFieldPlaceable.placeRelative( - 0, + leadingPlaceable?.width ?: 0, Alignment.CenterVertically.align(textFieldPlaceable.height, height), ) // placed similar to the input text above placeholderPlaceable?.placeRelative( - 0, + leadingPlaceable?.width ?: 0, Alignment.CenterVertically.align(placeholderPlaceable.height, height), ) } @@ -266,3 +291,4 @@ private fun Placeable.PlacementScope.place( private const val PLACEHOLDER_ID = "Placeholder" private const val TEXT_FIELD_ID = "TextField" private const val TRAILING_ID = "Trailing" +private const val LEADING_ID = "Leading" diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt b/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt new file mode 100644 index 000000000..b98bade98 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt @@ -0,0 +1,64 @@ +package org.jetbrains.jewel + +import androidx.compose.foundation.TooltipArea +import androidx.compose.foundation.TooltipPlacement +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.styling.TooltipStyle + +@Composable fun Tooltip( + tooltip: @Composable () -> Unit, + modifier: Modifier = Modifier, + tooltipPlacement: TooltipPlacement = TooltipPlacement.ComponentRect( + alignment = Alignment.CenterEnd, + anchor = Alignment.BottomEnd, + offset = DpOffset(4.dp, 4.dp), + ), + style: TooltipStyle = IntelliJTheme.tooltipStyle, + content: @Composable () -> Unit, +) { + TooltipArea( + tooltip = { + CompositionLocalProvider( + LocalContentColor provides style.colors.content, + ) { + Box( + modifier = Modifier + .shadow( + elevation = style.metrics.shadowSize, + shape = RoundedCornerShape(style.metrics.cornerSize), + ambientColor = style.colors.shadow, + spotColor = Color.Transparent, + ) + .background( + color = style.colors.background, + shape = RoundedCornerShape(style.metrics.cornerSize), + ) + .border( + width = style.metrics.borderWidth, + color = style.colors.border, + shape = RoundedCornerShape(style.metrics.cornerSize), + ) + .padding(style.metrics.contentPadding), + ) { + tooltip() + } + } + }, + modifier = modifier, + delayMillis = style.metrics.showDelay.inWholeMilliseconds.toInt(), + tooltipPlacement = tooltipPlacement, + content = content, + ) +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt index 1bc0aba2c..2e50e999d 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,6 +23,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import org.jetbrains.jewel.foundation.lazy.SelectableLazyListScopeContainer.Entry import org.jetbrains.jewel.foundation.tree.DefaultSelectableLazyColumnEventAction import org.jetbrains.jewel.foundation.tree.DefaultSelectableLazyColumnKeyActions @@ -48,9 +50,9 @@ fun SelectableLazyColumn( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: SelectableLazyListScope.() -> Unit, ) { - val container = remember(content) { - SelectableLazyListScopeContainer().apply(content) - } + val scope = rememberCoroutineScope() + val container = SelectableLazyListScopeContainer() + .apply(content) val keys = remember(container) { container.getKeys() @@ -75,7 +77,16 @@ fun SelectableLazyColumn( .focusable(interactionSource = interactionSource) .onPreviewKeyEvent { event -> if (state.lastActiveItemIndex != null) { - keyActions.handleOnKeyEvent(event, keys, state, selectionMode).invoke(event) + val actionHandled = keyActions + .handleOnKeyEvent(event, keys, state, selectionMode) + .invoke(event) + if (actionHandled) { + scope.launch { + state.lastActiveItemIndex?.let { + state.scrollToItem(it) + } + } + } } true }, diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListState.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListState.kt index 9768b435d..4bd726ba7 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListState.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListState.kt @@ -19,6 +19,7 @@ val SelectableLazyListState.visibleItemsRange get() = firstVisibleItemIndex..firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size interface SelectableScope { + var selectedKeys: List } @@ -41,16 +42,14 @@ class SelectableLazyListState( * @param itemIndex The index of the item to focus on. * @param animateScroll Whether to animate the scroll to the focused item. * @param scrollOffset The scroll offset for the focused item. - * @param skipScroll Whether to skip the scroll to the focused item. */ suspend fun scrollToItem( itemIndex: Int, animateScroll: Boolean = false, scrollOffset: Int = 0, - skipScroll: Boolean = false, ) { val visibleRange = visibleItemsRange.drop(2).dropLast(4) - if (!skipScroll && itemIndex !in visibleRange && visibleRange.isNotEmpty()) { + if (itemIndex !in visibleRange && visibleRange.isNotEmpty()) { when { itemIndex < visibleRange.first() -> lazyListState.scrollToItem( max(0, itemIndex - 2), @@ -59,7 +58,7 @@ class SelectableLazyListState( ) itemIndex > visibleRange.last() -> { - lazyListState.scrollToItem(max(itemIndex - (visibleRange.size + 1), 0), animateScroll, 0) + lazyListState.scrollToItem(max(itemIndex - (visibleRange.size + 2), 0), animateScroll, 0) } } } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/Tree.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/Tree.kt index 27e29cbd9..89649c200 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/Tree.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/Tree.kt @@ -1,14 +1,37 @@ package org.jetbrains.jewel.foundation.tree +import org.jetbrains.jewel.foundation.tree.Tree.Element.Node + @Suppress("UNCHECKED_CAST") fun emptyTree() = Tree.EMPTY as Tree -class Tree internal constructor(internal val roots: List>) { +class Tree internal constructor(val roots: List>) { companion object { internal val EMPTY = Tree(roots = emptyList>()) } + fun isEmpty() = roots.isEmpty() + + private fun walk(breathFirst: Boolean) = sequence { + val queue = roots.toMutableList() + while (queue.isNotEmpty()) { + val next = queue.removeFirst() + yield(next) + if (next is Node) { + next.open() + if (breathFirst) { + queue.addAll(next.children.orEmpty()) + } else { + queue.addAll(0, next.children.orEmpty()) + } + } + } + } + + fun walkBreadthFirst() = walk(true) + fun walkDepthFirst() = walk(false) + sealed interface Element { val data: T diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/CircularProgressStyle.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/CircularProgressStyle.kt new file mode 100644 index 000000000..ae3d34d42 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/CircularProgressStyle.kt @@ -0,0 +1,15 @@ +package org.jetbrains.jewel.styling + +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import kotlin.time.Duration + +interface CircularProgressStyle { + + val frameTime: Duration + val color: Color +} + +val LocalCircularProgressStyle = staticCompositionLocalOf { + error("No CircularProgressIndicatorStyle provided") +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/DividerStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/DividerStyling.kt new file mode 100644 index 000000000..67fcda051 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/DividerStyling.kt @@ -0,0 +1,24 @@ +package org.jetbrains.jewel.styling + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +@Immutable +interface DividerStyle { + + val color: Color + val metrics: DividerMetrics +} + +@Immutable +interface DividerMetrics { + + val thickness: Dp + val startIndent: Dp +} + +val LocalDividerStyle = staticCompositionLocalOf { + error("No DividerStyle provided") +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/GroupHeaderStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/GroupHeaderStyling.kt index c79b7d6e2..849327367 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/GroupHeaderStyling.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/GroupHeaderStyling.kt @@ -15,7 +15,6 @@ interface GroupHeaderStyle { @Immutable interface GroupHeaderColors { - val content: Color val divider: Color } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/IconButtonMetrics.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/IconButtonMetrics.kt new file mode 100644 index 000000000..c34923395 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/IconButtonMetrics.kt @@ -0,0 +1,71 @@ +package org.jetbrains.jewel.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import org.jetbrains.jewel.ButtonState + +@Stable +interface IconButtonStyle { + + val colors: IconButtonColors + val metrics: IconButtonMetrics +} + +@Immutable +interface IconButtonColors { + + val background: Color + val backgroundDisabled: Color + val backgroundFocused: Color + val backgroundPressed: Color + val backgroundHovered: Color + + val border: Color + val borderDisabled: Color + val borderFocused: Color + val borderPressed: Color + val borderHovered: Color + + @Composable + fun backgroundFor(state: ButtonState) = rememberUpdatedState( + when { + !state.isEnabled -> backgroundDisabled + state.isPressed -> backgroundPressed + state.isHovered -> backgroundHovered + state.isFocused -> backgroundFocused + else -> background + }, + ) + + @Composable + fun borderFor(state: ButtonState) = rememberUpdatedState( + when { + !state.isEnabled -> borderDisabled + state.isFocused -> borderFocused + state.isPressed -> borderPressed + state.isHovered -> borderHovered + else -> border + }, + ) +} + +@Stable +interface IconButtonMetrics { + + val cornerSize: CornerSize + val borderWidth: Dp + val padding: PaddingValues + val minSize: DpSize +} + +val LocalIconButtonStyle = staticCompositionLocalOf { + error("No IconButtonStyle provided") +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt index 60c8e720c..cb98285c6 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt @@ -2,17 +2,24 @@ package org.jetbrains.jewel.styling import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.ResourceLoader +import org.jetbrains.jewel.IconMapper +import org.jetbrains.jewel.IntelliJIconMapper +import org.jetbrains.jewel.IntelliJThemeIconData import org.jetbrains.jewel.InteractiveComponentState import org.jetbrains.jewel.InternalJewelApi +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.painterResource open class ResourcePainterProvider @InternalJewelApi constructor( private val basePath: String, private val svgLoader: SvgLoader, + private val iconMapper: IconMapper, + private val iconData: IntelliJThemeIconData, private val pathPatcher: ResourcePathPatcher, ) : PainterProvider { @@ -36,8 +43,11 @@ open class ResourcePainterProvider @InternalJewelApi constructor( basePath: String, resourceLoader: ResourceLoader, extraData: T?, - ): String = - pathPatcher.patchPath(basePath, resourceLoader, extraData) + ): String { + val patched = pathPatcher.patchVariant(basePath, resourceLoader, extraData) + val override = iconMapper.mapPath(patched, iconData, resourceLoader) + return pathPatcher.patchTheme(override, resourceLoader) + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -47,6 +57,8 @@ open class ResourcePainterProvider @InternalJewelApi constructor( if (basePath != other.basePath) return false if (svgLoader != other.svgLoader) return false + if (iconMapper != other.iconMapper) return false + if (iconData != other.iconData) return false if (pathPatcher != other.pathPatcher) return false return true @@ -55,24 +67,52 @@ open class ResourcePainterProvider @InternalJewelApi constructor( override fun hashCode(): Int { var result = basePath.hashCode() result = 31 * result + svgLoader.hashCode() + result = 31 * result + iconMapper.hashCode() + result = 31 * result + iconData.hashCode() result = 31 * result + pathPatcher.hashCode() return result } override fun toString(): String = - "ResourcePainterProvider(basePath='$basePath', svgLoader=$svgLoader, pathPatcher=$pathPatcher)" + "ResourcePainterProvider(basePath='$basePath', svgLoader=$svgLoader, iconMapper=$iconMapper, iconData=$iconData, pathPatcher=$pathPatcher)" @OptIn(InternalJewelApi::class) // These are the public constructors companion object Factory { - fun stateless(basePath: String, svgLoader: SvgLoader) = - ResourcePainterProvider(basePath, svgLoader, SimpleResourcePathPatcher()) + fun stateless(basePath: String, svgLoader: SvgLoader, iconData: IntelliJThemeIconData) = + ResourcePainterProvider( + basePath, + svgLoader, + IntelliJIconMapper, + iconData, + SimpleResourcePathPatcher(), + ) fun stateful( basePath: String, svgLoader: SvgLoader, + iconData: IntelliJThemeIconData, pathPatcher: ResourcePathPatcher = StatefulResourcePathPatcher(), ) = - ResourcePainterProvider(basePath, svgLoader, pathPatcher) + ResourcePainterProvider(basePath, svgLoader, IntelliJIconMapper, iconData, pathPatcher) } } + +@Composable +fun rememberStatelessPainterProvider( + basePath: String, + svgLoader: SvgLoader, + iconData: IntelliJThemeIconData = LocalIconData.current, +): ResourcePainterProvider = remember(basePath, iconData) { + ResourcePainterProvider.stateless(basePath, svgLoader, iconData) +} + +@Composable +fun rememberStatefulPainterProvider( + basePath: String, + svgLoader: SvgLoader, + pathPatcher: ResourcePathPatcher = StatefulResourcePathPatcher(), + iconData: IntelliJThemeIconData = LocalIconData.current, +): ResourcePainterProvider = remember(basePath, iconData, pathPatcher) { + ResourcePainterProvider.stateful(basePath, svgLoader, iconData, pathPatcher) +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePathPatcher.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePathPatcher.kt index 14a91ccf2..1974a9b92 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePathPatcher.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePathPatcher.kt @@ -6,5 +6,8 @@ import androidx.compose.ui.res.ResourceLoader interface ResourcePathPatcher { @Composable - fun patchPath(basePath: String, resourceLoader: ResourceLoader, extraData: T?): String + fun patchVariant(basePath: String, resourceLoader: ResourceLoader, extraData: T?): String + + @Composable + fun patchTheme(basePath: String, resourceLoader: ResourceLoader): String } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt index 83ea93361..072f81164 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt @@ -7,24 +7,35 @@ import org.jetbrains.jewel.LocalIsDarkTheme open class SimpleResourcePathPatcher : ResourcePathPatcher { @Composable - final override fun patchPath(basePath: String, resourceLoader: ResourceLoader, extraData: T?) = + final override fun patchVariant(basePath: String, resourceLoader: ResourceLoader, extraData: T?) = buildString { append(basePath.substringBeforeLast('/', "")) append('/') append(basePath.substringBeforeLast('.').substringAfterLast('/')) - append(injectAdditionalTokens(extraData)) + append(injectVariantTokens(extraData)) - // TODO load HiDPI rasterized images ("@2x") - // TODO load sized SVG images (e.g., "@20x20") - - if (LocalIsDarkTheme.current) { - append("_dark") - } append('.') append(basePath.substringAfterLast('.')) } @Composable - protected open fun injectAdditionalTokens(extraData: T? = null): String = "" + final override fun patchTheme(basePath: String, resourceLoader: ResourceLoader): String = buildString { + append(basePath.substringBeforeLast('/', "")) + append('/') + append(basePath.substringBeforeLast('.').substringAfterLast('/')) + + // TODO load HiDPI rasterized images ("@2x") + // TODO load sized SVG images (e.g., "@20x20") + + if (LocalIsDarkTheme.current) { + append("_dark") + } + + append('.') + append(basePath.substringAfterLast('.')) + } + + @Composable + protected open fun injectVariantTokens(extraData: T? = null): String = "" } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/StatefulResourcePathPatcher.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/StatefulResourcePathPatcher.kt index 3786914c7..6291d40cc 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/StatefulResourcePathPatcher.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/StatefulResourcePathPatcher.kt @@ -12,7 +12,7 @@ class StatefulResourcePathPatcher( ) : SimpleResourcePathPatcher() { @Composable - override fun injectAdditionalTokens(extraData: T?): String = buildString { + override fun injectVariantTokens(extraData: T?): String = buildString { if (extraData == null) return@buildString append(prefixTokensProvider(extraData)) diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt new file mode 100644 index 000000000..bc9bada87 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt @@ -0,0 +1,39 @@ +package org.jetbrains.jewel.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Stable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import kotlin.time.Duration + +@Stable +interface TooltipStyle { + + val colors: TooltipColors + val metrics: TooltipMetrics +} + +@Stable +interface TooltipColors { + + val background: Color + val content: Color + val border: Color + val shadow: Color +} + +@Stable +interface TooltipMetrics { + + val contentPadding: PaddingValues + val showDelay: Duration + val cornerSize: CornerSize + val borderWidth: Dp + val shadowSize: Dp +} + +val LocalTooltipStyle = staticCompositionLocalOf { + error("No TooltipStyle provided") +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt b/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt index 643477153..915a52a72 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt @@ -6,8 +6,8 @@ import org.jetbrains.jewel.PaletteMapper abstract class PaletteMapperFactory { protected fun createInternal( - iconColorPalette: Map, - keyPalette: Map, + iconColorPalette: Map, + keyPalette: Map, themeColors: Map, isDark: Boolean, ): PaletteMapper { @@ -20,15 +20,20 @@ abstract class PaletteMapperFactory { val map = selectMap(key, checkBoxes, trees, ui) ?: continue // If the value is one of the named colors in the theme, use that named color's value - val namedColor = themeColors.get(value) as? String - val resolvedValue = namedColor ?: value + val namedColor = themeColors[value]?.let { rawColor -> + when (rawColor) { + is Int -> Color(rawColor) + is String -> rawColor.toColorOrNull() + else -> null + } + } // If either the key or the resolved value aren't valid colors, ignore the entry val keyAsColor = resolveKeyColor(key, keyPalette, isDark) ?: continue - val resolvedValueAsColor = resolvedValue.toColorOrNull() ?: continue + val resolvedColor = namedColor ?: value?.toColorOrNull() ?: continue // Save the new entry (oldColor -> newColor) in the map - map[keyAsColor] = resolvedValueAsColor + map[keyAsColor] = resolvedColor } return PaletteMapper( @@ -39,7 +44,7 @@ abstract class PaletteMapperFactory { } // See com.intellij.ide.ui.UITheme.toColorString - private fun resolveKeyColor(key: String, keyPalette: Map, isDark: Boolean): Color? { + private fun resolveKeyColor(key: String, keyPalette: Map, isDark: Boolean): Color? { val darkKey = "$key.Dark" val resolvedKey = if (isDark && keyPalette.containsKey(darkKey)) darkKey else key return keyPalette[resolvedKey]?.toColorOrNull() diff --git a/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt b/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt index bd49dc09d..2c0687e91 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt @@ -71,9 +71,9 @@ object StandalonePaletteMapperFactory : PaletteMapperFactory() { isDark = isDark, ) - private fun Map.asColorStringsMap() = + private fun Map.asColorStringsMap(): Map = mapValues { (_, color) -> - "#${color.toArgb().toString(16).padStart(6, '0')}" + color.toArgb() } override fun logInfo(message: String) { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt b/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt new file mode 100644 index 000000000..f2da077e4 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt @@ -0,0 +1,22 @@ +package org.jetbrains.jewel.util + +import androidx.compose.ui.graphics.Color +import kotlin.math.roundToInt + +fun Color.toHexString(): String { + val r = Integer.toHexString((red * 255).roundToInt()) + val g = Integer.toHexString((green * 255).roundToInt()) + val b = Integer.toHexString((blue * 255).roundToInt()) + + return buildString { + append('#') + append(r.padStart(2, '0')) + append(g.padStart(2, '0')) + append(b.padStart(2, '0')) + + if (alpha != 1.0f) { + val a = Integer.toHexString((alpha * 255).roundToInt()) + append(a.padStart(2, '0')) + } + } +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/util/SpinnerProgressIconGenerator.kt b/core/src/main/kotlin/org/jetbrains/jewel/util/SpinnerProgressIconGenerator.kt new file mode 100644 index 000000000..578428e9e --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/jewel/util/SpinnerProgressIconGenerator.kt @@ -0,0 +1,102 @@ +package org.jetbrains.jewel.util + +object SpinnerProgressIconGenerator { + + private val opacityList = listOf(1.0f, 0.93f, 0.78f, 0.69f, 0.62f, 0.48f, 0.38f, 0.0f) + + private val rotations = listOf(0, -45, 0, 45, 0, -45, 0, 45) + + // for a 16x16 icon + internal val points = listOf( + 7f to 1f, + 2.34961f to 3.76416f, + 1f to 7f, + 5.17871f to 9.40991f, + 7f to 11f, + 9.41016f to 10.8242f, + 11f to 7f, + 12.2383f to 2.34961f, + ) + + private fun StringBuilder.closeTag() = append("") + private fun StringBuilder.openTag(sizePx: Int) = append( + "", + ) + + private fun getSvgPlainTextIcon( + step: Int, + pointList: List>, + colorHex: String, + thickness: Int = 2, + length: Int = 4, + cornerRadius: Int = 1, + ) = + buildString { + openTag(16) + appendLine() + for (index in 0..opacityList.lastIndex) { + val currentIndex = (index + step + 1) % opacityList.size + val currentOpacity = opacityList[currentIndex] + if (currentOpacity == 0.0f) continue + drawElement( + colorHex = colorHex, + opacity = currentOpacity, + x = pointList[index].first, + y = pointList[index].second, + width = thickness, + height = length, + rx = cornerRadius, + rotation = rotations[index], + ) + } + closeTag() + appendLine() + } + + private fun StringBuilder.drawElement( + colorHex: String, + opacity: Float, + x: Float, + y: Float, + width: Int, + height: Int, + rx: Int, + rotation: Int, + ) { + append("\n") + } + + internal fun getPlainTextSvgList(colorHex: String, size: Int) = buildList { + val scaleFactor = size / 16f + for (index in 0..opacityList.lastIndex) { + if (size == 16) { + add(getSvgPlainTextIcon(index, points, colorHex)) + } else { + add( + getSvgPlainTextIcon( + index, + points.map { it.first * scaleFactor to it.second * scaleFactor }, + colorHex, + thickness = (2 * scaleFactor).toInt().coerceAtLeast(1), + length = (4 * scaleFactor).toInt().coerceAtLeast(1), + cornerRadius = (2 * scaleFactor).toInt().coerceAtLeast(1), + ), + ) + } + } + } + + object Small { + + fun generateRawSvg(colorHex: String) = getPlainTextSvgList(colorHex = colorHex, size = 16) + } + + object Big { + + fun generateRawSvg(colorHex: String) = + getPlainTextSvgList(colorHex = colorHex, size = 32) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63039cdbb..370a2df4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,9 @@ [versions] -composeDesktop = "1.5.1" +composeDesktop = "1.5.2" coroutines = "1.7.3" detekt = "1.23.1" -idea = "232.8660.185" +idea232 = "232.8660.185" +idea233 = "233.9102.97-EAP-SNAPSHOT" ideaGradlePlugin = "1.15.0" javaSarif = "2.0" kotlinSarif = "0.4.0" @@ -11,15 +12,24 @@ dokka = "1.8.20" kotlinterGradlePlugin = "3.16.0" kotlinxSerialization = "1.5.1" kotlinpoet = "1.14.2" +semVer = "1.2.0" +simpleXml = "2.7.1" [libraries] javaSarif = { module = "com.contrastsecurity:java-sarif", version.ref = "javaSarif" } kotlinSarif = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "kotlinSarif" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -ij-platform-ide-core = { module = "com.jetbrains.intellij.platform:ide-core", version.ref = "idea" } -ij-platform-ide-impl = { module = "com.jetbrains.intellij.platform:ide-impl", version.ref = "idea" } -ij-platform-core-ui = { module = "com.jetbrains.intellij.platform:core-ui", version.ref = "idea" } + +ij-platform-ide-core-232 = { module = "com.jetbrains.intellij.platform:ide-core", version.ref = "idea232" } +ij-platform-ide-impl-232 = { module = "com.jetbrains.intellij.platform:ide-impl", version.ref = "idea232" } +ij-platform-core-ui-232 = { module = "com.jetbrains.intellij.platform:core-ui", version.ref = "idea232" } + +ij-platform-ide-core-233 = { module = "com.jetbrains.intellij.platform:ide-core", version.ref = "idea233" } +ij-platform-ide-impl-233 = { module = "com.jetbrains.intellij.platform:ide-impl", version.ref = "idea233" } +ij-platform-core-ui-233 = { module = "com.jetbrains.intellij.platform:core-ui", version.ref = "idea233" } +semVer = { module = "net.swiftzer.semver:semver", version.ref = "semVer" } +simpleXml = { module = "org.simpleframework:simple-xml", version.ref = "simpleXml" } # Plugin libraries for build-logic's convention plugins to use to resolve the types/tasks coming from these plugins detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } @@ -29,7 +39,8 @@ dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", versi kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } [bundles] -idea = ["ij-platform-ide-core", "ij-platform-ide-impl", "ij-platform-core-ui"] +idea232 = ["ij-platform-ide-core-232", "ij-platform-ide-impl-232", "ij-platform-core-ui-232"] +idea233 = ["ij-platform-ide-core-233", "ij-platform-ide-impl-233", "ij-platform-core-ui-233"] [plugins] composeDesktop = { id = "org.jetbrains.compose", version.ref = "composeDesktop" } diff --git a/ide-laf-bridge/build.gradle.kts b/ide-laf-bridge/build.gradle.kts index 263d0525e..5ebda090e 100644 --- a/ide-laf-bridge/build.gradle.kts +++ b/ide-laf-bridge/build.gradle.kts @@ -1,12 +1,29 @@ +import SupportedIJVersion.* + plugins { jewel - `jewel-publish` + `jewel-ij-publish` alias(libs.plugins.composeDesktop) } dependencies { - api(projects.intUi.intUiStandalone) - compileOnly(libs.bundles.idea) + api(projects.intUi.intUiCore) { + exclude(group = "org.jetbrains.kotlinx") + } + when (supportedIJVersion()) { + IJ_232 -> { + api(projects.ideLafBridge.ideLafBridge232) + compileOnly(libs.bundles.idea232) + } + + IJ_233 -> { + api(projects.ideLafBridge.ideLafBridge233) + compileOnly(libs.bundles.idea233) + } + } testImplementation(compose.desktop.uiTestJUnit4) -} + testImplementation(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + } +} \ No newline at end of file diff --git a/ide-laf-bridge/ide-laf-bridge-232/build.gradle.kts b/ide-laf-bridge/ide-laf-bridge-232/build.gradle.kts new file mode 100644 index 000000000..cb3d89359 --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-232/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + jewel + `jewel-ij-publish` + alias(libs.plugins.composeDesktop) +} + +dependencies { + api(projects.intUi.intUiStandalone) + compileOnly(libs.bundles.idea232) +} + +tasks.withType().configureEach { + publication.artifactId = "jewel-ide-laf-bridge-platform-specific" + enabled = supportedIJVersion() == SupportedIJVersion.IJ_232 +} \ No newline at end of file diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt similarity index 82% rename from ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt rename to ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt index 3c69af4a1..ea6ff34ee 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt +++ b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt @@ -2,13 +2,16 @@ package org.jetbrains.jewel.bridge import androidx.compose.runtime.Immutable import com.intellij.ide.ui.UITheme +import com.intellij.ui.ColorUtil import org.jetbrains.jewel.IntelliJThemeIconData +import org.jetbrains.jewel.InternalJewelApi @Immutable -internal class BridgeIconData( +@InternalJewelApi +class BridgeIconData( override val iconOverrides: Map, override val colorPalette: Map, - override val selectionColorPalette: Map, + override val selectionColorPalette: Map, ) : IntelliJThemeIconData { override fun equals(other: Any?): Boolean { @@ -37,10 +40,13 @@ internal class BridgeIconData( companion object { + @OptIn(InternalJewelApi::class) fun readFromLaF(): BridgeIconData { val uiTheme = currentUiThemeOrNull() val iconMap = uiTheme?.icons.orEmpty() - val selectedIconColorPalette = uiTheme?.selectedIconColorPalette.orEmpty() + val selectedIconColorPalette = uiTheme?.selectedIconColorPalette.orEmpty().mapValues { + ColorUtil.fromHex(it.value).rgb + } val colorPalette = UITheme.getColorPalette() return BridgeIconData(iconMap, colorPalette, selectedIconColorPalette) diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt similarity index 100% rename from ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt rename to ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt diff --git a/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/IconsPathPatching.kt b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/IconsPathPatching.kt new file mode 100644 index 000000000..dea03a894 --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/IconsPathPatching.kt @@ -0,0 +1,29 @@ +package org.jetbrains.jewel.bridge + +import com.intellij.util.ui.DirProvider +import org.jetbrains.jewel.InternalJewelApi + +@InternalJewelApi +fun getPatchedIconPath( + dirProvider: DirProvider, + originalPath: String, + classLoaders: List, +): String? { + val clazz = Class.forName("com.intellij.ui.icons.CachedImageIconKt") + val patchIconPath = clazz.getMethod("patchIconPath", String::class.java, ClassLoader::class.java) + patchIconPath.isAccessible = true + + // For all provided classloaders, we try to get the patched path, both using + // the original path, and an "abridged" path that has gotten the icon path prefix + // removed (the classloader is set up differently in prod IDEs and when running + // from Gradle, and the icon could be in either place depending on the environment) + val fallbackPath = originalPath.removePrefix(dirProvider.dir()) + val patchedPath = classLoaders.firstNotNullOfOrNull { classLoader -> + val patchedPathAndClassLoader = + patchIconPath.invoke(null, originalPath.removePrefix("/"), classLoader) + ?: patchIconPath.invoke(null, fallbackPath, classLoader) + patchedPathAndClassLoader as? Pair<*, *> + }?.first as? String + + return patchedPath +} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt similarity index 91% rename from ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt rename to ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt index 61c0f4dc2..cee7a675b 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt +++ b/ide-laf-bridge/ide-laf-bridge-232/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt @@ -4,6 +4,7 @@ import com.intellij.ide.ui.LafManager import com.intellij.ide.ui.UITheme import com.intellij.ide.ui.laf.UIThemeBasedLookAndFeelInfo import com.intellij.openapi.diagnostic.Logger +import org.jetbrains.jewel.InternalJewelApi import java.lang.reflect.Field private val logger = Logger.getInstance("UiThemeExtensions") @@ -11,11 +12,14 @@ private val logger = Logger.getInstance("UiThemeExtensions") private val classUITheme get() = UITheme::class.java +@Suppress("UnstableApiUsage") +@InternalJewelApi internal fun currentUiThemeOrNull() = (LafManager.getInstance().currentLookAndFeel as? UIThemeBasedLookAndFeelInfo)?.theme // TODO #116 replace with public API access once it's made available (IJP 233?) -internal val UITheme.icons: Map +@InternalJewelApi +val UITheme.icons: Map get() = readMapField(classUITheme.getDeclaredField("icons")) .filterKeys { it != "ColorPalette" } diff --git a/ide-laf-bridge/ide-laf-bridge-233/build.gradle.kts b/ide-laf-bridge/ide-laf-bridge-233/build.gradle.kts new file mode 100644 index 000000000..743a7e8e7 --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-233/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + jewel + `jewel-ij-publish` + alias(libs.plugins.composeDesktop) +} + +dependencies { + api(projects.intUi.intUiStandalone) + compileOnly(libs.bundles.idea233) +} + +tasks.withType().configureEach { + publication.artifactId = "jewel-ide-laf-bridge-platform-specific" + enabled = supportedIJVersion() == SupportedIJVersion.IJ_233 +} \ No newline at end of file diff --git a/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt new file mode 100644 index 000000000..0078240f5 --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt @@ -0,0 +1,53 @@ +package org.jetbrains.jewel.bridge + +import androidx.compose.runtime.Immutable +import com.intellij.ide.ui.UITheme +import org.jetbrains.jewel.IntelliJThemeIconData +import org.jetbrains.jewel.InternalJewelApi + +@Immutable +@InternalJewelApi +class BridgeIconData( + override val iconOverrides: Map, + override val colorPalette: Map, + override val selectionColorPalette: Map, +) : IntelliJThemeIconData { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BridgeIconData + + if (iconOverrides != other.iconOverrides) return false + if (colorPalette != other.colorPalette) return false + if (selectionColorPalette != other.selectionColorPalette) return false + + return true + } + + override fun hashCode(): Int { + var result = iconOverrides.hashCode() + result = 31 * result + colorPalette.hashCode() + result = 31 * result + selectionColorPalette.hashCode() + return result + } + + override fun toString(): String = + "BridgeIconData(iconOverrides=$iconOverrides, colorPalette=$colorPalette, " + + "selectionColorPalette=$selectionColorPalette)" + + companion object { + + @Suppress("UnstableApiUsage") + fun readFromLaF(): BridgeIconData { + val uiTheme = currentUiThemeOrNull() + val bean = uiTheme?.describe() + val iconMap = bean?.icons.orEmpty() + val selectedIconColorPalette = bean?.iconColorsOnSelection.orEmpty() + + val colorPalette = UITheme.getColorPalette() + return BridgeIconData(iconMap, colorPalette, selectedIconColorPalette) + } + } +} diff --git a/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt new file mode 100644 index 000000000..d29ed438f --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt @@ -0,0 +1,38 @@ +package org.jetbrains.jewel.bridge + +import com.intellij.ide.ui.UITheme +import com.intellij.openapi.diagnostic.thisLogger +import org.jetbrains.jewel.PaletteMapper +import org.jetbrains.jewel.themes.PaletteMapperFactory + +object BridgePaletteMapperFactory : PaletteMapperFactory() { + + private val logger = thisLogger() + + @Suppress("UnstableApiUsage") + fun create(isDark: Boolean): PaletteMapper { + // If we can't read the current theme, no mapping is possible + val uiTheme = currentUiThemeOrNull() ?: return PaletteMapper.Empty + logger.info("Parsing theme info from theme ${uiTheme.name} (id: ${uiTheme.id}, isDark: ${uiTheme.isDark})") + + val bean = uiTheme.describe() + + // TODO: in New UI + Dark theme values can be non String, but IJColorUIResource + // since these are deserialized values simply casted to Map. + // Let's handle it while it is not fixed in platform + val iconColorPalette = (bean.colorPalette as Map).mapValues { + when (val value = it.value) { + is String -> value + else -> null + } + } + val keyPalette = UITheme.getColorPalette() + val themeColors = bean.colors + + return createInternal(iconColorPalette, keyPalette, themeColors, isDark) + } + + override fun logInfo(message: String) { + logger.info(message) + } +} diff --git a/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/IconsPathPatching.kt b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/IconsPathPatching.kt new file mode 100644 index 000000000..52aba2360 --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/IconsPathPatching.kt @@ -0,0 +1,29 @@ +package org.jetbrains.jewel.bridge + +import com.intellij.ui.icons.patchIconPath +import com.intellij.util.ui.DirProvider +import org.jetbrains.jewel.InternalJewelApi + +@InternalJewelApi +@Suppress("UnstableApiUsage") +fun getPatchedIconPath( + dirProvider: DirProvider, + originalPath: String, + classLoaders: List, +): String? { + // For all provided classloaders, we try to get the patched path, both using + // the original path, and an "abridged" path that has gotten the icon path prefix + // removed (the classloader is set up differently in prod IDEs and when running + // from Gradle, and the icon could be in either place depending on the environment) + val fallbackPath = originalPath.removePrefix(dirProvider.dir()) + + for (classLoader in classLoaders) { + val patchedPath = patchIconPath(originalPath.removePrefix("/"), classLoader)?.first + ?: patchIconPath(fallbackPath, classLoader)?.first + + if (patchedPath != null) { + return patchedPath + } + } + return null +} diff --git a/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt new file mode 100644 index 000000000..81a6cd6da --- /dev/null +++ b/ide-laf-bridge/ide-laf-bridge-233/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt @@ -0,0 +1,8 @@ +package org.jetbrains.jewel.bridge + +import com.intellij.ide.ui.LafManager +import com.intellij.ide.ui.laf.UIThemeLookAndFeelInfo + +@Suppress("UnstableApiUsage") +internal fun currentUiThemeOrNull(): UIThemeLookAndFeelInfo? = + LafManager.getInstance().currentUIThemeLookAndFeel?.takeIf { it.isInitialized } diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeGlobalMetrics.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeGlobalMetrics.kt index f4183eb44..52ce20ce5 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeGlobalMetrics.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeGlobalMetrics.kt @@ -4,12 +4,15 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.intellij.ide.ui.laf.darcula.DarculaUIUtil +import com.intellij.ui.scale.JBUIScale +import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import org.jetbrains.jewel.GlobalMetrics @Immutable internal class BridgeGlobalMetrics( override val outlineWidth: Dp, + override val rowHeight: Dp, ) : GlobalMetrics { override fun equals(other: Any?): Boolean { @@ -19,14 +22,19 @@ internal class BridgeGlobalMetrics( other as BridgeGlobalMetrics if (outlineWidth != other.outlineWidth) return false + if (rowHeight != other.rowHeight) return false return true } - override fun hashCode(): Int = outlineWidth.hashCode() + override fun hashCode(): Int { + var result = outlineWidth.hashCode() + result = 31 * result + rowHeight.hashCode() + return result + } - override fun toString(): String = - "BridgeGlobalMetrics(outlineWidth=$outlineWidth)" + override fun toString() = + "BridgeGlobalMetrics(outlineWidth=$outlineWidth, rowHeight=$rowHeight)" companion object { @@ -36,7 +44,11 @@ internal class BridgeGlobalMetrics( val f = if (UIUtil.isRetina()) 0.5f else 1.0f val lw = if (UIUtil.isUnderDefaultMacTheme()) f else DarculaUIUtil.LW.unscaled - return BridgeGlobalMetrics(outlineWidth = (DarculaUIUtil.BW.unscaled + lw).dp) + return BridgeGlobalMetrics( + outlineWidth = (DarculaUIUtil.BW.unscaled + lw).dp, + // The rowHeight() function returns a scaled value, but we need the base value + rowHeight = (JBUI.CurrentTheme.List.rowHeight() / JBUIScale.scale(1f)).dp, + ) } } } diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt index 9c85bef76..d7f3cbbe0 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt @@ -4,7 +4,9 @@ import androidx.compose.ui.res.ResourceLoader import com.intellij.openapi.diagnostic.thisLogger import com.intellij.util.ui.DirProvider import org.jetbrains.jewel.ClassLoaderProvider +import org.jetbrains.jewel.IconMapper import org.jetbrains.jewel.IntelliJThemeIconData +import org.jetbrains.jewel.InternalJewelApi internal object BridgeIconMapper : IconMapper { @@ -12,6 +14,7 @@ internal object BridgeIconMapper : IconMapper { private val dirProvider = DirProvider() + @OptIn(InternalJewelApi::class) override fun mapPath( originalPath: String, iconData: IntelliJThemeIconData, @@ -26,23 +29,7 @@ internal object BridgeIconMapper : IconMapper { return originalPath } - // TODO #116 replace with public API access once it's made available (IJP 233?) - val clazz = Class.forName("com.intellij.ui.icons.CachedImageIconKt") - val patchIconPath = clazz.getMethod("patchIconPath", String::class.java, ClassLoader::class.java) - patchIconPath.isAccessible = true - - // For all provided classloaders, we try to get the patched path, both using - // the original path, and an "abridged" path that has gotten the icon path prefix - // removed (the classloader is set up differently in prod IDEs and when running - // from Gradle, and the icon could be in either place depending on the environment) - val fallbackPath = originalPath.removePrefix(dirProvider.dir()) - val patchedPath = classLoaders.firstNotNullOfOrNull { classLoader -> - val patchedPathAndClassLoader = - patchIconPath.invoke(null, originalPath.removePrefix("/"), classLoader) - ?: patchIconPath.invoke(null, fallbackPath, classLoader) - patchedPathAndClassLoader as? Pair<*, *> - }?.first as? String - + val patchedPath = getPatchedIconPath(dirProvider, originalPath, classLoaders) val path = if (patchedPath != null) { logger.info("Found icon mapping: '$originalPath' -> '$patchedPath'") patchedPath diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt index 82c271645..cdbb3de33 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt @@ -1,7 +1,6 @@ package org.jetbrains.jewel.bridge -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.ResourceLoader +import org.jetbrains.jewel.IconMapper import org.jetbrains.jewel.IntelliJThemeIconData import org.jetbrains.jewel.InteractiveComponentState import org.jetbrains.jewel.InternalJewelApi @@ -16,19 +15,9 @@ internal class BridgeResourcePainterProvider @InternalJewelApi constructor( basePath: String, svgLoader: SvgLoader, pathPatcher: ResourcePathPatcher, - private val iconMapper: IconMapper, - private val iconData: IntelliJThemeIconData, -) : ResourcePainterProvider(basePath, svgLoader, pathPatcher) { - - @Composable - override fun patchPath( - basePath: String, - resourceLoader: ResourceLoader, - extraData: T?, - ): String { - val patchedPath = super.patchPath(basePath, resourceLoader, extraData) - return iconMapper.mapPath(patchedPath, iconData, resourceLoader) - } + iconMapper: IconMapper, + iconData: IntelliJThemeIconData, +) : ResourcePainterProvider(basePath, svgLoader, iconMapper, iconData, pathPatcher) { companion object Factory { diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt index 3fd20bdf1..f1aaedc9e 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.platform.Typeface import androidx.compose.ui.unit.Dp @@ -15,20 +16,26 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.takeOrElse import com.intellij.openapi.diagnostic.Logger import com.intellij.ui.JBColor +import com.intellij.ui.JBColor.marker import com.intellij.ui.scale.JBUIScale.scale import com.intellij.util.ui.JBDimension +import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBInsets import com.intellij.util.ui.JBValue +import kotlinx.coroutines.runBlocking import org.jetbrains.jewel.IntelliJThemeIconData import org.jetbrains.jewel.InteractiveComponentState import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.styling.PainterProvider +import org.jetbrains.skia.Typeface import org.jetbrains.skiko.DependsOnJBR import org.jetbrains.skiko.awt.font.AwtFontManager import org.jetbrains.skiko.toSkikoTypefaceOrNull import java.awt.Dimension +import java.awt.Font import java.awt.Insets import javax.swing.UIManager @@ -43,11 +50,15 @@ fun java.awt.Color.toComposeColor() = Color( fun java.awt.Color?.toComposeColorOrUnspecified() = this?.toComposeColor() ?: Color.Unspecified -@Suppress("UnstableApiUsage") -fun retrieveColorOrNull(key: String) = - JBColor.namedColor(key) - .takeUnless { it.name == "NAMED_COLOR_FALLBACK_MARKER" } - ?.toComposeColor() +fun retrieveColorOrNull(key: String): Color? = + try { + JBColor.namedColor(key, marker("JEWEL_JBCOLOR_MARKER")).toComposeColor() + } catch (_: AssertionError) { + // JBColor.marker will throw AssertionError on getRGB/any other color + // for now there is no way to handle non-existing key. + // The way should be introduced in platform + null + } fun retrieveColorOrUnspecified(key: String): Color { val color = retrieveColorOrNull(key) @@ -159,28 +170,44 @@ suspend fun retrieveTextStyle( key: String, color: Color = Color.Unspecified, lineHeight: TextUnit = TextUnit.Unspecified, + bold: Boolean = false, + fontStyle: FontStyle = FontStyle.Normal, + size: TextUnit = TextUnit.Unspecified, ): TextStyle { - val font = UIManager.getFont(key) ?: keyNotFound(key, "Font") - - return with(font) { - val typeface = toSkikoTypefaceOrNull(awtFontManager) - ?: org.jetbrains.skia.Typeface.makeDefault() - .also { - logger.warn( - "Unable to convert font ${font.fontName} into a Skiko typeface, " + - "fallback to 'Typeface.makeDefault()'", - ) - } - - TextStyle( - color = color, - fontSize = size.sp, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily(Typeface(typeface)), - // todo textDecoration might be defined in the awt theme - lineHeight = lineHeight, - ) - } + val font = JBFont.create( + UIManager.getFont(key) ?: keyNotFound(key, "Font"), + false, + ) + + val derivedFont = font.let { if (bold) it.asBold() else it.asPlain() } + .let { if (fontStyle == FontStyle.Italic) it.asItalic() else it } + + val typeface = derivedFont.toSkikoTypefaceOrNull(awtFontManager) + ?: Typeface.makeDefault() + .also { + logger.warn( + "Unable to convert font ${font.fontName} into a Skiko typeface, " + + "fallback to 'Typeface.makeDefault()'", + ) + } + + return TextStyle( + color = color, + fontSize = size.takeOrElse { derivedFont.size.sp }, + fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal, + fontStyle = fontStyle, + fontFamily = FontFamily(Typeface(typeface)), + // todo textDecoration might be defined in the awt theme + lineHeight = lineHeight, + ) +} + +@DependsOnJBR +fun Font.toFontFamily(): FontFamily { + val typeface = runBlocking { toSkikoTypefaceOrNull(awtFontManager) } + ?: error("Can't turn $this into a Typeface") + + return FontFamily(Typeface(typeface)) } val JBValue.dp diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt deleted file mode 100644 index da88ea8f6..000000000 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.jetbrains.jewel.bridge - -import androidx.compose.ui.res.ResourceLoader -import org.jetbrains.jewel.IntelliJThemeIconData - -interface IconMapper { - - fun mapPath(originalPath: String, iconData: IntelliJThemeIconData, resourceLoader: ResourceLoader): String -} diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt index 62ff507fe..f79e1f3c6 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.TextStyle @@ -36,6 +37,9 @@ import org.jetbrains.jewel.intui.standalone.styling.IntUiCheckboxStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiChipColors import org.jetbrains.jewel.intui.standalone.styling.IntUiChipMetrics import org.jetbrains.jewel.intui.standalone.styling.IntUiChipStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiCircularProgressStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiDividerMetrics +import org.jetbrains.jewel.intui.standalone.styling.IntUiDividerStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownColors import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownIcons import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownMetrics @@ -46,6 +50,9 @@ import org.jetbrains.jewel.intui.standalone.styling.IntUiGroupHeaderStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiHorizontalProgressBarColors import org.jetbrains.jewel.intui.standalone.styling.IntUiHorizontalProgressBarMetrics import org.jetbrains.jewel.intui.standalone.styling.IntUiHorizontalProgressBarStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonColors +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonMetrics +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiLabelledTextFieldColors import org.jetbrains.jewel.intui.standalone.styling.IntUiLabelledTextFieldMetrics import org.jetbrains.jewel.intui.standalone.styling.IntUiLabelledTextFieldStyle @@ -84,6 +91,9 @@ import org.jetbrains.jewel.intui.standalone.styling.IntUiTextAreaStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiTextFieldColors import org.jetbrains.jewel.intui.standalone.styling.IntUiTextFieldMetrics import org.jetbrains.jewel.intui.standalone.styling.IntUiTextFieldStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiTooltipColors +import org.jetbrains.jewel.intui.standalone.styling.IntUiTooltipMetrics +import org.jetbrains.jewel.intui.standalone.styling.IntUiTooltipStyle import org.jetbrains.jewel.styling.InputFieldStyle import org.jetbrains.skiko.DependsOnJBR import javax.swing.UIManager @@ -112,6 +122,7 @@ internal fun createBridgeIntUiDefinition(textStyle: TextStyle): IntUiThemeDefini iconData = BridgeIconData.readFromLaF(), globalMetrics = BridgeGlobalMetrics.readFromLaF(), defaultTextStyle = textStyle, + contentColor = JBColor.foreground().toComposeColor(), ) } @@ -148,6 +159,7 @@ internal fun createSwingIntUiComponentStyling( chipStyle = readChipStyle(), defaultButtonStyle = readDefaultButtonStyle(), defaultTabStyle = readDefaultTabStyle(theme.iconData, svgLoader), + dividerStyle = readDividerStyle(), dropdownStyle = readDropdownStyle(theme.iconData, svgLoader, menuStyle, dropdownTextStyle), editorTabStyle = readEditorTabStyle(theme.iconData, svgLoader), groupHeaderStyle = readGroupHeaderStyle(), @@ -160,7 +172,10 @@ internal fun createSwingIntUiComponentStyling( radioButtonStyle = readRadioButtonStyle(theme.iconData, svgLoader), scrollbarStyle = readScrollbarStyle(theme.isDark), textAreaStyle = readTextAreaStyle(textAreaTextStyle, textFieldStyle.metrics), + circularProgressStyle = readCircularProgressStyle(theme.isDark), + tooltipStyle = readTooltipStyle(), textFieldStyle = textFieldStyle, + iconButtonStyle = readIconButtonStyle(), ) } @@ -337,6 +352,12 @@ private fun readChipStyle(): IntUiChipStyle { ) } +private fun readDividerStyle() = + IntUiDividerStyle( + color = retrieveColorOrUnspecified("Borders.color"), + metrics = IntUiDividerMetrics(), + ) + private fun readDropdownStyle( iconData: IntelliJThemeIconData, svgLoader: SvgLoader, @@ -398,7 +419,6 @@ private fun readDropdownStyle( private fun readGroupHeaderStyle() = IntUiGroupHeaderStyle( colors = IntUiGroupHeaderColors( - content = retrieveColorOrUnspecified("Separator.foreground"), divider = retrieveColorOrUnspecified("Separator.separatorColor"), ), metrics = IntUiGroupHeaderMetrics( @@ -614,12 +634,12 @@ private fun readRadioButtonStyle(iconData: IntelliJThemeIconData, svgLoader: Svg private fun readScrollbarStyle(isDark: Boolean) = IntUiScrollbarStyle( colors = IntUiScrollbarColors( - // See ScrollBarPainter.THUMB_BACKGROUND + // See ScrollBarPainter.THUMB_OPAQUE_BACKGROUND thumbBackground = retrieveColorOrUnspecified("ScrollBar.Mac.Transparent.thumbColor") - .takeOrElse { if (isDark) Color(0x00000000) else Color(0x00808080) }, - // See ScrollBarPainter.THUMB_HOVERED_BACKGROUND + .takeOrElse { if (isDark) Color(0x59808080) else Color(0x33000000) }, + // See ScrollBarPainter.THUMB_OPAQUE_HOVERED_BACKGROUND thumbBackgroundHovered = retrieveColorOrUnspecified("ScrollBar.Mac.Transparent.hoverThumbColor") - .takeOrElse { if (isDark) Color(0x00000000) else Color(0x00808080) }, + .takeOrElse { if (isDark) Color(0x8C808080) else Color(0x80000000) }, ), metrics = IntUiScrollbarMetrics( thumbCornerSize = CornerSize(100), @@ -881,3 +901,41 @@ private fun readEditorTabStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLo ), ) } + +private fun readCircularProgressStyle( + isDark: Boolean, +): IntUiCircularProgressStyle = + IntUiCircularProgressStyle( + frameTime = 125.milliseconds, + color = retrieveColorOrUnspecified("ProgressIcon.color") + .takeIf { it.isSpecified } + ?: if (isDark) Color(0xFF6F737A) else Color(0xFFA8ADBD), + ) + +private fun readTooltipStyle(): IntUiTooltipStyle { + return IntUiTooltipStyle( + metrics = IntUiTooltipMetrics(), + colors = IntUiTooltipColors( + content = retrieveColorOrUnspecified("ToolTip.foreground"), + background = retrieveColorOrUnspecified("ToolTip.background"), + border = retrieveColorOrUnspecified("ToolTip.borderColor"), + shadow = retrieveColorOrUnspecified("Notification.Shadow.bottom1Color"), + ), + ) +} + +private fun readIconButtonStyle(): IntUiIconButtonStyle = IntUiIconButtonStyle( + metrics = IntUiIconButtonMetrics(CornerSize(DarculaUIUtil.BUTTON_ARC.dp / 2)), + colors = IntUiIconButtonColors( + background = Color.Unspecified, + backgroundDisabled = Color.Unspecified, + backgroundFocused = Color.Unspecified, + backgroundPressed = retrieveColorOrUnspecified("ActionButton.pressedBackground"), + backgroundHovered = retrieveColorOrUnspecified("ActionButton.hoverBackground"), + border = Color.Unspecified, + borderDisabled = Color.Unspecified, + borderFocused = retrieveColorOrUnspecified("ActionButton.focusedBorderColor"), + borderPressed = retrieveColorOrUnspecified("ActionButton.pressedBorderColor"), + borderHovered = retrieveColorOrUnspecified("ActionButton.hoverBorderColor"), + ), +) diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt index c172fa978..d05162840 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt @@ -9,7 +9,8 @@ import org.jetbrains.jewel.ExperimentalJewelApi import org.jetbrains.jewel.LocalResourceLoader import org.jetbrains.jewel.intui.standalone.IntUiTheme -private val bridgeService = service() +private val bridgeService + get() = service() @ExperimentalJewelApi @Composable diff --git a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/BaseIntUiTheme.kt b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/BaseIntUiTheme.kt index 1d3d569ca..01464dec5 100644 --- a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/BaseIntUiTheme.kt +++ b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/BaseIntUiTheme.kt @@ -1,15 +1,11 @@ package org.jetbrains.jewel.intui.core -import androidx.compose.foundation.Indication -import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.LocalContextMenuRepresentation import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.text.TextStyle import org.jetbrains.jewel.GlobalColors import org.jetbrains.jewel.GlobalMetrics @@ -19,9 +15,12 @@ import org.jetbrains.jewel.IntelliJTheme import org.jetbrains.jewel.IntelliJThemeIconData import org.jetbrains.jewel.LocalColorPalette import org.jetbrains.jewel.LocalIconData +import org.jetbrains.jewel.NoIndication import org.jetbrains.jewel.styling.ButtonStyle import org.jetbrains.jewel.styling.CheckboxStyle import org.jetbrains.jewel.styling.ChipStyle +import org.jetbrains.jewel.styling.CircularProgressStyle +import org.jetbrains.jewel.styling.DividerStyle import org.jetbrains.jewel.styling.DropdownStyle import org.jetbrains.jewel.styling.GroupHeaderStyle import org.jetbrains.jewel.styling.HorizontalProgressBarStyle @@ -30,12 +29,15 @@ import org.jetbrains.jewel.styling.LazyTreeStyle import org.jetbrains.jewel.styling.LinkStyle import org.jetbrains.jewel.styling.LocalCheckboxStyle import org.jetbrains.jewel.styling.LocalChipStyle +import org.jetbrains.jewel.styling.LocalCircularProgressStyle import org.jetbrains.jewel.styling.LocalDefaultButtonStyle import org.jetbrains.jewel.styling.LocalDefaultTabStyle +import org.jetbrains.jewel.styling.LocalDividerStyle import org.jetbrains.jewel.styling.LocalDropdownStyle import org.jetbrains.jewel.styling.LocalEditorTabStyle import org.jetbrains.jewel.styling.LocalGroupHeaderStyle import org.jetbrains.jewel.styling.LocalHorizontalProgressBarStyle +import org.jetbrains.jewel.styling.LocalIconButtonStyle import org.jetbrains.jewel.styling.LocalLabelledTextFieldStyle import org.jetbrains.jewel.styling.LocalLazyTreeStyle import org.jetbrains.jewel.styling.LocalLinkStyle @@ -45,6 +47,7 @@ import org.jetbrains.jewel.styling.LocalRadioButtonStyle import org.jetbrains.jewel.styling.LocalScrollbarStyle import org.jetbrains.jewel.styling.LocalTextAreaStyle import org.jetbrains.jewel.styling.LocalTextFieldStyle +import org.jetbrains.jewel.styling.LocalTooltipStyle import org.jetbrains.jewel.styling.MenuStyle import org.jetbrains.jewel.styling.RadioButtonStyle import org.jetbrains.jewel.styling.ScrollbarStyle @@ -54,9 +57,6 @@ import org.jetbrains.jewel.styling.TextFieldStyle interface BaseIntUiTheme : IntelliJTheme { - val defaultLightTextStyle: TextStyle - val defaultDarkTextStyle: TextStyle - val globalColors: GlobalColors @Composable @ReadOnlyComposable @@ -67,10 +67,10 @@ interface BaseIntUiTheme : IntelliJTheme { @ReadOnlyComposable get() = IntelliJTheme.globalMetrics - val defaultTextStyle: TextStyle + val textStyle: TextStyle @Composable @ReadOnlyComposable - get() = IntelliJTheme.defaultTextStyle + get() = IntelliJTheme.textStyle val contentColor: Color @Composable @@ -117,6 +117,11 @@ interface BaseIntUiTheme : IntelliJTheme { @ReadOnlyComposable get() = IntelliJTheme.chipStyle + val dividerStyle: DividerStyle + @Composable + @ReadOnlyComposable + get() = IntelliJTheme.dividerStyle + val dropdownStyle: DropdownStyle @Composable @ReadOnlyComposable @@ -181,6 +186,11 @@ interface BaseIntUiTheme : IntelliJTheme { @Composable @ReadOnlyComposable get() = IntelliJTheme.editorTabStyle + + val circularProgressStyle: CircularProgressStyle + @Composable + @ReadOnlyComposable + get() = IntelliJTheme.circularProgressStyle } @Composable @@ -206,6 +216,7 @@ fun BaseIntUiTheme( LocalChipStyle provides componentStyling.chipStyle, LocalContextMenuRepresentation provides IntelliJContextMenuRepresentation, LocalDefaultButtonStyle provides componentStyling.defaultButtonStyle, + LocalDividerStyle provides componentStyling.dividerStyle, LocalDropdownStyle provides componentStyling.dropdownStyle, LocalGroupHeaderStyle provides componentStyling.groupHeaderStyle, LocalHorizontalProgressBarStyle provides componentStyling.horizontalProgressBarStyle, @@ -221,21 +232,10 @@ fun BaseIntUiTheme( LocalDefaultTabStyle provides componentStyling.defaultTabStyle, LocalEditorTabStyle provides componentStyling.editorTabStyle, LocalIndication provides NoIndication, + LocalCircularProgressStyle provides componentStyling.circularProgressStyle, + LocalTooltipStyle provides componentStyling.tooltipStyle, + LocalIconButtonStyle provides componentStyling.iconButtonStyle, ) { IntelliJTheme(theme, swingCompatMode, content) } } - -private object NoIndication : Indication { - - private object NoIndicationInstance : IndicationInstance { - - override fun ContentDrawScope.drawIndication() { - drawContent() - } - } - - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance = - NoIndicationInstance -} diff --git a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt index cb042a0bd..dfbe84266 100644 --- a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt +++ b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel.intui.core import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import org.jetbrains.jewel.GlobalColors import org.jetbrains.jewel.GlobalMetrics @@ -15,6 +16,7 @@ class IntUiThemeDefinition( override val iconData: IntelliJThemeIconData, override val globalMetrics: GlobalMetrics, override val defaultTextStyle: TextStyle, + override val contentColor: Color, ) : IntelliJThemeDefinition { override fun equals(other: Any?): Boolean { diff --git a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntelliJSvgPatcher.kt b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntelliJSvgPatcher.kt index 7ca6db7f5..dfa88a4a6 100644 --- a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntelliJSvgPatcher.kt +++ b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntelliJSvgPatcher.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import org.jetbrains.jewel.PaletteMapper import org.jetbrains.jewel.SvgPatcher +import org.jetbrains.jewel.util.toHexString import org.w3c.dom.Document import org.w3c.dom.Element import java.io.IOException @@ -63,24 +64,6 @@ class IntelliJSvgPatcher(private val mapper: PaletteMapper) : SvgPatcher { } } - private fun Color.toHexString(): String { - val r = Integer.toHexString((red * 255).roundToInt()) - val g = Integer.toHexString((green * 255).roundToInt()) - val b = Integer.toHexString((blue * 255).roundToInt()) - - return buildString { - append('#') - append(r.padStart(2, '0')) - append(g.padStart(2, '0')) - append(b.padStart(2, '0')) - - if (alpha != 1.0f) { - val a = Integer.toHexString((alpha * 255).roundToInt()) - append(a.padStart(2, '0')) - } - } - } - private fun tryParseColor(color: String, alpha: Float): Color? { val rawColor = color.lowercase() if (rawColor.startsWith("#") && rawColor.length - 1 <= 8) { diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiGlobalMetrics.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiGlobalMetrics.kt index 17e77e865..5f2b75678 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiGlobalMetrics.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiGlobalMetrics.kt @@ -8,6 +8,7 @@ import org.jetbrains.jewel.GlobalMetrics @Immutable class IntUiGlobalMetrics( override val outlineWidth: Dp = 2.dp, + override val rowHeight: Dp = 24.dp, ) : GlobalMetrics { override fun equals(other: Any?): Boolean { @@ -16,11 +17,18 @@ class IntUiGlobalMetrics( other as IntUiGlobalMetrics - return outlineWidth == other.outlineWidth + if (outlineWidth != other.outlineWidth) return false + if (rowHeight != other.rowHeight) return false + + return true } - override fun hashCode(): Int = outlineWidth.hashCode() + override fun hashCode(): Int { + var result = outlineWidth.hashCode() + result = 31 * result + rowHeight.hashCode() + return result + } - override fun toString(): String = - "IntUiGlobalMetrics(outlineWidth=$outlineWidth)" + override fun toString() = + "IntUiGlobalMetrics(outlineWidth=$outlineWidth, rowHeight=$rowHeight)" } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiTheme.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiTheme.kt index 0b5fb934b..f3d94c5d0 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiTheme.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/IntUiTheme.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -34,9 +35,12 @@ import org.jetbrains.jewel.intui.standalone.IntUiTheme.defaultComponentStyling import org.jetbrains.jewel.intui.standalone.styling.IntUiButtonStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiCheckboxStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiChipStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiCircularProgressStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiDividerStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiGroupHeaderStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiHorizontalProgressBarStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiLabelledTextFieldStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiLazyTreeStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiLinkStyle @@ -46,12 +50,16 @@ import org.jetbrains.jewel.intui.standalone.styling.IntUiScrollbarStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiTabStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiTextAreaStyle import org.jetbrains.jewel.intui.standalone.styling.IntUiTextFieldStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiTooltipStyle import org.jetbrains.jewel.styling.ButtonStyle import org.jetbrains.jewel.styling.CheckboxStyle import org.jetbrains.jewel.styling.ChipStyle +import org.jetbrains.jewel.styling.CircularProgressStyle +import org.jetbrains.jewel.styling.DividerStyle import org.jetbrains.jewel.styling.DropdownStyle import org.jetbrains.jewel.styling.GroupHeaderStyle import org.jetbrains.jewel.styling.HorizontalProgressBarStyle +import org.jetbrains.jewel.styling.IconButtonStyle import org.jetbrains.jewel.styling.LabelledTextFieldStyle import org.jetbrains.jewel.styling.LazyTreeStyle import org.jetbrains.jewel.styling.LinkStyle @@ -64,24 +72,22 @@ import org.jetbrains.jewel.themes.StandalonePaletteMapperFactory object IntUiTheme : BaseIntUiTheme { - private val intUiDefaultTextStyle = TextStyle.Default.copy( + val defaultTextStyle = TextStyle.Default.copy( fontFamily = FontFamily.Inter, fontSize = 13.sp, fontWeight = FontWeight.Normal, fontStyle = FontStyle.Normal, ) - override val defaultLightTextStyle = intUiDefaultTextStyle.copy(color = IntUiLightTheme.colors.grey(1)) - override val defaultDarkTextStyle = intUiDefaultTextStyle.copy(color = IntUiDarkTheme.colors.grey(12)) - @Composable fun lightThemeDefinition( colors: GlobalColors = IntUiGlobalColors.light(), metrics: GlobalMetrics = IntUiGlobalMetrics(), palette: IntUiThemeColorPalette = IntUiLightTheme.colors, icons: IntelliJThemeIconData = IntUiLightTheme.icons, - defaultTextStyle: TextStyle = defaultLightTextStyle, - ) = IntUiThemeDefinition(isDark = false, colors, palette, icons, metrics, defaultTextStyle) + defaultTextStyle: TextStyle = this.defaultTextStyle, + contentColor: Color = IntUiLightTheme.colors.grey(1), + ) = IntUiThemeDefinition(isDark = false, colors, palette, icons, metrics, defaultTextStyle, contentColor) @Composable fun darkThemeDefinition( @@ -89,8 +95,9 @@ object IntUiTheme : BaseIntUiTheme { metrics: GlobalMetrics = IntUiGlobalMetrics(), palette: IntUiThemeColorPalette = IntUiDarkTheme.colors, icons: IntelliJThemeIconData = IntUiDarkTheme.icons, - defaultTextStyle: TextStyle = defaultDarkTextStyle, - ) = IntUiThemeDefinition(isDark = true, colors, palette, icons, metrics, defaultTextStyle) + defaultTextStyle: TextStyle = this.defaultTextStyle, + contentColor: Color = IntUiDarkTheme.colors.grey(12), + ) = IntUiThemeDefinition(isDark = true, colors, palette, icons, metrics, defaultTextStyle, contentColor) @Composable fun defaultComponentStyling(theme: IntUiThemeDefinition, svgLoader: SvgLoader): IntelliJComponentStyling { @@ -115,6 +122,7 @@ object IntUiTheme : BaseIntUiTheme { outlinedButtonStyle: ButtonStyle = IntUiButtonStyle.Outlined.dark(), checkboxStyle: CheckboxStyle = IntUiCheckboxStyle.dark(svgLoader), chipStyle: ChipStyle = IntUiChipStyle.dark(), + dividerStyle: DividerStyle = IntUiDividerStyle.dark(), dropdownStyle: DropdownStyle = IntUiDropdownStyle.dark(svgLoader), groupHeaderStyle: GroupHeaderStyle = IntUiGroupHeaderStyle.dark(), labelledTextFieldStyle: LabelledTextFieldStyle = IntUiLabelledTextFieldStyle.dark(), @@ -128,12 +136,16 @@ object IntUiTheme : BaseIntUiTheme { lazyTreeStyle: LazyTreeStyle = IntUiLazyTreeStyle.dark(svgLoader), defaultTabStyle: TabStyle = IntUiTabStyle.Default.dark(svgLoader), editorTabStyle: TabStyle = IntUiTabStyle.Editor.dark(svgLoader), + circularProgressStyle: CircularProgressStyle = IntUiCircularProgressStyle.dark(), + tooltipStyle: IntUiTooltipStyle = IntUiTooltipStyle.dark(), + iconButtonStyle: IconButtonStyle = IntUiIconButtonStyle.dark(), ) = IntelliJComponentStyling( checkboxStyle = checkboxStyle, chipStyle = chipStyle, defaultButtonStyle = defaultButtonStyle, defaultTabStyle = defaultTabStyle, + dividerStyle = dividerStyle, dropdownStyle = dropdownStyle, editorTabStyle = editorTabStyle, groupHeaderStyle = groupHeaderStyle, @@ -147,6 +159,9 @@ object IntUiTheme : BaseIntUiTheme { scrollbarStyle = scrollbarStyle, textAreaStyle = textAreaStyle, textFieldStyle = textFieldStyle, + circularProgressStyle = circularProgressStyle, + tooltipStyle = tooltipStyle, + iconButtonStyle = iconButtonStyle, ) @Composable @@ -156,6 +171,7 @@ object IntUiTheme : BaseIntUiTheme { outlinedButtonStyle: ButtonStyle = IntUiButtonStyle.Outlined.light(), checkboxStyle: CheckboxStyle = IntUiCheckboxStyle.light(svgLoader), chipStyle: ChipStyle = IntUiChipStyle.light(), + dividerStyle: DividerStyle = IntUiDividerStyle.light(), dropdownStyle: DropdownStyle = IntUiDropdownStyle.light(svgLoader), groupHeaderStyle: GroupHeaderStyle = IntUiGroupHeaderStyle.light(), labelledTextFieldStyle: LabelledTextFieldStyle = IntUiLabelledTextFieldStyle.light(), @@ -169,26 +185,32 @@ object IntUiTheme : BaseIntUiTheme { lazyTreeStyle: LazyTreeStyle = IntUiLazyTreeStyle.light(svgLoader), defaultTabStyle: TabStyle = IntUiTabStyle.Default.light(svgLoader), editorTabStyle: TabStyle = IntUiTabStyle.Editor.light(svgLoader), - ) = - IntelliJComponentStyling( - checkboxStyle = checkboxStyle, - chipStyle = chipStyle, - defaultButtonStyle = defaultButtonStyle, - defaultTabStyle = defaultTabStyle, - dropdownStyle = dropdownStyle, - editorTabStyle = editorTabStyle, - groupHeaderStyle = groupHeaderStyle, - horizontalProgressBarStyle = horizontalProgressBarStyle, - labelledTextFieldStyle = labelledTextFieldStyle, - lazyTreeStyle = lazyTreeStyle, - linkStyle = linkStyle, - menuStyle = menuStyle, - outlinedButtonStyle = outlinedButtonStyle, - radioButtonStyle = radioButtonStyle, - scrollbarStyle = scrollbarStyle, - textAreaStyle = textAreaStyle, - textFieldStyle = textFieldStyle, - ) + circularProgressStyle: CircularProgressStyle = IntUiCircularProgressStyle.light(), + tooltipStyle: IntUiTooltipStyle = IntUiTooltipStyle.light(), + iconButtonStyle: IconButtonStyle = IntUiIconButtonStyle.light(), + ) = IntelliJComponentStyling( + checkboxStyle = checkboxStyle, + chipStyle = chipStyle, + defaultButtonStyle = defaultButtonStyle, + defaultTabStyle = defaultTabStyle, + dividerStyle = dividerStyle, + dropdownStyle = dropdownStyle, + editorTabStyle = editorTabStyle, + groupHeaderStyle = groupHeaderStyle, + horizontalProgressBarStyle = horizontalProgressBarStyle, + labelledTextFieldStyle = labelledTextFieldStyle, + lazyTreeStyle = lazyTreeStyle, + linkStyle = linkStyle, + menuStyle = menuStyle, + outlinedButtonStyle = outlinedButtonStyle, + radioButtonStyle = radioButtonStyle, + scrollbarStyle = scrollbarStyle, + textAreaStyle = textAreaStyle, + textFieldStyle = textFieldStyle, + circularProgressStyle = circularProgressStyle, + tooltipStyle = tooltipStyle, + iconButtonStyle = iconButtonStyle, + ) } @Composable @@ -197,21 +219,40 @@ fun IntUiTheme( swingCompatMode: Boolean = false, content: @Composable () -> Unit, ) { - val svgLoader by remember(themeDefinition.isDark, themeDefinition.iconData, themeDefinition.colorPalette) { + val svgLoader by rememberSvgLoader( + isDark = themeDefinition.isDark, + iconData = themeDefinition.iconData, + colorPalette = themeDefinition.colorPalette, + ) + + val componentStyling = defaultComponentStyling(themeDefinition, svgLoader) + IntUiTheme(themeDefinition, componentStyling, swingCompatMode, content) +} + +/** + * Create and remember an instance of [SvgLoader]. + * + * Note that since [SvgLoader] may cache the loaded images, and that + * creating it may be somewhat expensive, you should only create it once at + * the top level, and pass it around. + */ +@Composable +fun rememberSvgLoader( + isDark: Boolean = IntUiTheme.isDark, + iconData: IntelliJThemeIconData = IntUiTheme.iconData, + colorPalette: IntUiThemeColorPalette = IntUiTheme.colorPalette, +) = + remember(isDark, iconData, colorPalette) { val paletteMapper = StandalonePaletteMapperFactory.create( - themeDefinition.isDark, - themeDefinition.iconData, - themeDefinition.colorPalette, + isDark, + iconData, + colorPalette, ) val svgPatcher = IntelliJSvgPatcher(paletteMapper) mutableStateOf(JewelSvgLoader(svgPatcher)) } - val componentStyling = defaultComponentStyling(themeDefinition, svgLoader) - IntUiTheme(themeDefinition, componentStyling, swingCompatMode, content) -} - @Composable fun IntUiTheme( theme: IntUiThemeDefinition, diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt index a0a711fa5..b35d06664 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import org.jetbrains.jewel.CheckboxState +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme @@ -21,8 +22,7 @@ import org.jetbrains.jewel.styling.PainterProvider import org.jetbrains.jewel.styling.ResourcePainterProvider import org.jetbrains.jewel.styling.StatefulResourcePathPatcher -@Immutable -data class IntUiCheckboxStyle( +@Immutable data class IntUiCheckboxStyle( override val colors: IntUiCheckboxColors, override val metrics: IntUiCheckboxMetrics, override val icons: IntUiCheckboxIcons, @@ -30,26 +30,23 @@ data class IntUiCheckboxStyle( companion object { - @Composable - fun light( + @Composable fun light( svgLoader: SvgLoader, colors: IntUiCheckboxColors = IntUiCheckboxColors.light(), metrics: IntUiCheckboxMetrics = IntUiCheckboxMetrics(), - icons: IntUiCheckboxIcons = intUiCheckboxIcons(svgLoader), + icons: IntUiCheckboxIcons = IntUiCheckboxIcons.light(svgLoader), ) = IntUiCheckboxStyle(colors, metrics, icons) - @Composable - fun dark( + @Composable fun dark( svgLoader: SvgLoader, colors: IntUiCheckboxColors = IntUiCheckboxColors.dark(), metrics: IntUiCheckboxMetrics = IntUiCheckboxMetrics(), - icons: IntUiCheckboxIcons = intUiCheckboxIcons(svgLoader), + icons: IntUiCheckboxIcons = IntUiCheckboxIcons.dark(svgLoader), ) = IntUiCheckboxStyle(colors, metrics, icons) } } -@Immutable -data class IntUiCheckboxColors( +@Immutable data class IntUiCheckboxColors( override val checkboxBackground: Color, override val checkboxBackgroundDisabled: Color, override val checkboxBackgroundSelected: Color, @@ -60,12 +57,11 @@ data class IntUiCheckboxColors( companion object { - @Composable - fun light( + @Composable fun light( background: Color = IntUiLightTheme.colors.grey(14), backgroundDisabled: Color = IntUiLightTheme.colors.grey(13), backgroundSelected: Color = IntUiLightTheme.colors.blue(4), - content: Color = IntUiLightTheme.colors.grey(1), + content: Color = Color.Unspecified, contentDisabled: Color = IntUiLightTheme.colors.grey(8), contentSelected: Color = content, ) = IntUiCheckboxColors( @@ -77,12 +73,11 @@ data class IntUiCheckboxColors( contentSelected, ) - @Composable - fun dark( + @Composable fun dark( background: Color = Color.Unspecified, backgroundDisabled: Color = IntUiDarkTheme.colors.grey(3), backgroundSelected: Color = IntUiDarkTheme.colors.blue(6), - content: Color = IntUiDarkTheme.colors.grey(12), + content: Color = Color.Unspecified, contentDisabled: Color = IntUiDarkTheme.colors.grey(7), contentSelected: Color = content, ) = IntUiCheckboxColors( @@ -96,8 +91,7 @@ data class IntUiCheckboxColors( } } -@Immutable -data class IntUiCheckboxMetrics( +@Immutable data class IntUiCheckboxMetrics( override val checkboxSize: DpSize = DpSize(19.dp, 19.dp), override val checkboxCornerSize: CornerSize = CornerSize(3.dp), override val outlineSize: DpSize = DpSize(15.dp, 15.dp), @@ -105,30 +99,43 @@ data class IntUiCheckboxMetrics( override val iconContentGap: Dp = 5.dp, ) : CheckboxMetrics -@Immutable -data class IntUiCheckboxIcons( +@Immutable data class IntUiCheckboxIcons( override val checkbox: PainterProvider, ) : CheckboxIcons { companion object { + @Composable fun checkbox( svgLoader: SvgLoader, - basePath: String = "icons/intui/checkBox.svg", - ): PainterProvider = - ResourcePainterProvider.stateful( - basePath, + basePath: String = "com/intellij/ide/ui/laf/icons/intellij/checkBox.svg", + ): PainterProvider = ResourcePainterProvider.stateful( + basePath, + svgLoader, + LocalIconData.current, + pathPatcher = StatefulResourcePathPatcher( + prefixTokensProvider = { state: CheckboxState -> + if (state.toggleableState == ToggleableState.Indeterminate) "Indeterminate" else "" + }, + ), + ) + + @Composable + fun light( + svgLoader: SvgLoader, + checkbox: PainterProvider = checkbox( + svgLoader, + "com/intellij/ide/ui/laf/icons/intellij/checkBox.svg", + ), + ) = IntUiCheckboxIcons(checkbox) + + @Composable + fun dark( + svgLoader: SvgLoader, + checkbox: PainterProvider = checkbox( svgLoader, - pathPatcher = StatefulResourcePathPatcher( - prefixTokensProvider = { state: CheckboxState -> - if (state.toggleableState == ToggleableState.Indeterminate) "Indeterminate" else "" - }, - ), - ) + "com/intellij/ide/ui/laf/icons/darcula/checkBox.svg", + ), + ) = IntUiCheckboxIcons(checkbox) } } - -fun intUiCheckboxIcons( - svgLoader: SvgLoader, - checkbox: PainterProvider = IntUiCheckboxIcons.checkbox(svgLoader), -) = IntUiCheckboxIcons(checkbox) diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCircularProgressStyle.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCircularProgressStyle.kt new file mode 100644 index 000000000..20d99bb97 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCircularProgressStyle.kt @@ -0,0 +1,27 @@ +package org.jetbrains.jewel.intui.standalone.styling + +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import org.jetbrains.jewel.styling.CircularProgressStyle +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +@Stable +data class IntUiCircularProgressStyle( + override val frameTime: Duration, + override val color: Color, +) : CircularProgressStyle { + + companion object { + + fun dark( + frameTime: Duration = 125.milliseconds, + color: Color = Color(0xFF6F737A), + ) = IntUiCircularProgressStyle(frameTime, color) + + fun light( + frameTime: Duration = 125.milliseconds, + color: Color = Color(0xFFA8ADBD), + ) = IntUiCircularProgressStyle(frameTime, color) + } +} diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDividerStyle.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDividerStyle.kt new file mode 100644 index 000000000..2d92dfce0 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDividerStyle.kt @@ -0,0 +1,39 @@ +package org.jetbrains.jewel.intui.standalone.styling + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme +import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.styling.DividerMetrics +import org.jetbrains.jewel.styling.DividerStyle + +@Immutable +class IntUiDividerStyle( + override val color: Color, + override val metrics: DividerMetrics, +) : DividerStyle { + + companion object { + + @Composable + fun light( + color: Color = IntUiLightTheme.colors.grey(12), + metrics: IntUiDividerMetrics = IntUiDividerMetrics(), + ) = IntUiDividerStyle(color, metrics) + + @Composable + fun dark( + color: Color = IntUiDarkTheme.colors.grey(1), + metrics: IntUiDividerMetrics = IntUiDividerMetrics(), + ) = IntUiDividerStyle(color, metrics) + } +} + +@Immutable +class IntUiDividerMetrics( + override val thickness: Dp = 1.dp, + override val startIndent: Dp = 0.dp, +) : DividerMetrics diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt index b14f6f6db..1ce35fb14 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import org.jetbrains.jewel.DropdownState +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme @@ -40,7 +41,7 @@ data class IntUiDropdownStyle( colors: IntUiDropdownColors = IntUiDropdownColors.light(), metrics: IntUiDropdownMetrics = IntUiDropdownMetrics(), icons: IntUiDropdownIcons = intUiDropdownIcons(svgLoader), - textStyle: TextStyle = IntUiTheme.defaultLightTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, menuStyle: MenuStyle = IntUiMenuStyle.light(svgLoader), ) = IntUiDropdownStyle(colors, metrics, icons, textStyle, menuStyle) @@ -50,7 +51,7 @@ data class IntUiDropdownStyle( colors: IntUiDropdownColors = IntUiDropdownColors.dark(), metrics: IntUiDropdownMetrics = IntUiDropdownMetrics(), icons: IntUiDropdownIcons = intUiDropdownIcons(svgLoader), - textStyle: TextStyle = IntUiTheme.defaultDarkTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, menuStyle: MenuStyle = IntUiMenuStyle.dark(svgLoader), ) = IntUiDropdownStyle(colors, metrics, icons, textStyle, menuStyle) } @@ -193,9 +194,9 @@ data class IntUiDropdownIcons( @Composable fun chevronDown( svgLoader: SvgLoader, - basePath: String = "icons/intui/chevronDown.svg", + basePath: String = "expui/general/chevronDown.svg", ): PainterProvider = - ResourcePainterProvider.stateful(basePath, svgLoader) + ResourcePainterProvider.stateful(basePath, svgLoader, LocalIconData.current) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiGroupHeaderStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiGroupHeaderStyling.kt index f25582164..1f316cee8 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiGroupHeaderStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiGroupHeaderStyling.kt @@ -35,7 +35,6 @@ data class IntUiGroupHeaderStyle( @Immutable data class IntUiGroupHeaderColors( - override val content: Color, override val divider: Color, ) : GroupHeaderColors { @@ -43,15 +42,13 @@ data class IntUiGroupHeaderColors( @Composable fun light( - content: Color = IntUiLightTheme.colors.grey(1), divider: Color = IntUiLightTheme.colors.grey(12), - ) = IntUiGroupHeaderColors(content, divider) + ) = IntUiGroupHeaderColors(divider) @Composable fun dark( - content: Color = IntUiDarkTheme.colors.grey(12), divider: Color = IntUiDarkTheme.colors.grey(3), - ) = IntUiGroupHeaderColors(content, divider) + ) = IntUiGroupHeaderColors(divider) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiIconButtonStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiIconButtonStyling.kt new file mode 100644 index 000000000..4d212b117 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiIconButtonStyling.kt @@ -0,0 +1,110 @@ +package org.jetbrains.jewel.intui.standalone.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme +import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.styling.IconButtonColors +import org.jetbrains.jewel.styling.IconButtonMetrics +import org.jetbrains.jewel.styling.IconButtonStyle + +@Stable +data class IntUiIconButtonStyle( + override val colors: IntUiIconButtonColors, + override val metrics: IntUiIconButtonMetrics, +) : IconButtonStyle { + + companion object { + + @Composable + fun light() = IntUiIconButtonStyle(IntUiIconButtonColors.light(), IntUiIconButtonMetrics()) + + @Composable + fun dark() = IntUiIconButtonStyle(IntUiIconButtonColors.dark(), IntUiIconButtonMetrics()) + } +} + +@Immutable +data class IntUiIconButtonColors( + override val background: Color, + override val backgroundDisabled: Color, + override val backgroundFocused: Color, + override val backgroundPressed: Color, + override val backgroundHovered: Color, + override val border: Color, + override val borderDisabled: Color, + override val borderFocused: Color, + override val borderPressed: Color, + override val borderHovered: Color, +) : IconButtonColors { + + companion object { + + @Composable + fun light( + background: Color = Color.Unspecified, + backgroundDisabled: Color = background, + backgroundFocused: Color = background, + backgroundPressed: Color = IntUiLightTheme.colors.grey(11), + backgroundHovered: Color = IntUiLightTheme.colors.grey(12), + border: Color = background, + borderDisabled: Color = border, + borderFocused: Color = IntUiLightTheme.colors.blue(5), + borderPressed: Color = backgroundPressed, + borderHovered: Color = backgroundHovered, + ) = + IntUiIconButtonColors( + background, + backgroundDisabled, + backgroundFocused, + backgroundPressed, + backgroundHovered, + border, + borderDisabled, + borderFocused, + borderPressed, + borderHovered, + ) + + @Composable + fun dark( + background: Color = Color.Unspecified, + backgroundDisabled: Color = background, + backgroundFocused: Color = background, + backgroundPressed: Color = IntUiDarkTheme.colors.grey(5), + backgroundHovered: Color = IntUiDarkTheme.colors.grey(3), + border: Color = background, + borderDisabled: Color = border, + borderFocused: Color = IntUiDarkTheme.colors.blue(6), + borderPressed: Color = backgroundPressed, + borderHovered: Color = backgroundHovered, + ) = + IntUiIconButtonColors( + background, + backgroundDisabled, + backgroundFocused, + backgroundPressed, + backgroundHovered, + border, + borderDisabled, + borderFocused, + borderPressed, + borderHovered, + ) + } +} + +@Stable +data class IntUiIconButtonMetrics( + override val cornerSize: CornerSize = CornerSize(4.dp), + override val borderWidth: Dp = 1.dp, + override val padding: PaddingValues = PaddingValues(0.dp), + override val minSize: DpSize = DpSize(16.dp, 16.dp), +) : IconButtonMetrics diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLabelledTextFieldStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLabelledTextFieldStyling.kt index 1ea4be9f1..c581ae57b 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLabelledTextFieldStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLabelledTextFieldStyling.kt @@ -33,7 +33,7 @@ data class IntUiLabelledTextFieldStyle( fun light( colors: IntUiLabelledTextFieldColors = IntUiLabelledTextFieldColors.light(), metrics: IntUiLabelledTextFieldMetrics = IntUiLabelledTextFieldMetrics(), - textStyle: TextStyle = IntUiTheme.defaultLightTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, textStyles: IntUiLabelledTextFieldTextStyles = IntUiLabelledTextFieldTextStyles.light(), ) = IntUiLabelledTextFieldStyle(colors, metrics, textStyle, textStyles) @@ -41,7 +41,7 @@ data class IntUiLabelledTextFieldStyle( fun dark( colors: IntUiLabelledTextFieldColors = IntUiLabelledTextFieldColors.dark(), metrics: IntUiLabelledTextFieldMetrics = IntUiLabelledTextFieldMetrics(), - textStyle: TextStyle = IntUiTheme.defaultDarkTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, textStyles: IntUiLabelledTextFieldTextStyles = IntUiLabelledTextFieldTextStyles.dark(), ) = IntUiLabelledTextFieldStyle(colors, metrics, textStyle, textStyles) } @@ -99,7 +99,7 @@ data class IntUiLabelledTextFieldColors( caretPressed: Color = caret, caretHovered: Color = caret, placeholder: Color = IntUiLightTheme.colors.grey(8), - label: Color = IntUiLightTheme.colors.grey(1), + label: Color = Color.Unspecified, hint: Color = IntUiLightTheme.colors.grey(6), ) = IntUiLabelledTextFieldColors( background, @@ -150,7 +150,7 @@ data class IntUiLabelledTextFieldColors( caretPressed: Color = caret, caretHovered: Color = caret, placeholder: Color = IntUiDarkTheme.colors.grey(7), - label: Color = IntUiDarkTheme.colors.grey(12), + label: Color = Color.Unspecified, hint: Color = IntUiDarkTheme.colors.grey(7), ) = IntUiLabelledTextFieldColors( background, @@ -200,8 +200,8 @@ data class IntUiLabelledTextFieldTextStyles( @Composable fun light( - label: TextStyle = IntUiTheme.defaultLightTextStyle, - hint: TextStyle = IntUiTheme.defaultLightTextStyle.copy( + label: TextStyle = IntUiTheme.defaultTextStyle, + hint: TextStyle = IntUiTheme.defaultTextStyle.copy( fontSize = 12.sp, lineHeight = 16.sp, ), @@ -209,8 +209,8 @@ data class IntUiLabelledTextFieldTextStyles( @Composable fun dark( - label: TextStyle = IntUiTheme.defaultDarkTextStyle, - hint: TextStyle = IntUiTheme.defaultDarkTextStyle.copy( + label: TextStyle = IntUiTheme.defaultTextStyle, + hint: TextStyle = IntUiTheme.defaultTextStyle.copy( fontSize = 12.sp, lineHeight = 16.sp, ), diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt index 1c83d7ef0..dec1abdf9 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme @@ -60,7 +61,7 @@ data class IntUiLazyTreeColors( @Composable fun light( - content: Color = IntUiLightTheme.colors.grey(1), + content: Color = Color.Unspecified, contentFocused: Color = content, contentSelected: Color = content, contentSelectedFocused: Color = content, @@ -79,7 +80,7 @@ data class IntUiLazyTreeColors( @Composable fun dark( - content: Color = IntUiDarkTheme.colors.grey(12), + content: Color = Color.Unspecified, contentFocused: Color = content, contentSelected: Color = content, contentSelectedFocused: Color = content, @@ -121,16 +122,16 @@ data class IntUiLazyTreeIcons( @Composable fun chevronCollapsed( svgLoader: SvgLoader, - basePath: String = "icons/intui/chevronRight.svg", + basePath: String = "expui/general/chevronRight.svg", ): PainterProvider = - ResourcePainterProvider.stateless(basePath, svgLoader) + ResourcePainterProvider.stateless(basePath, svgLoader, LocalIconData.current) @Composable fun chevronExpanded( svgLoader: SvgLoader, - basePath: String = "icons/intui/chevronDown.svg", + basePath: String = "expui/general/chevronDown.svg", ): PainterProvider = - ResourcePainterProvider.stateless(basePath, svgLoader) + ResourcePainterProvider.stateless(basePath, svgLoader, LocalIconData.current) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLinkStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLinkStyling.kt index 224077275..74af02e39 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLinkStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLinkStyling.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import org.jetbrains.jewel.LinkState +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme @@ -118,16 +119,16 @@ data class IntUiLinkIcons( @Composable fun dropdownChevron( svgLoader: SvgLoader, - basePath: String = "icons/intui/chevronDown.svg", + basePath: String = "expui/general/chevronDown.svg", ): PainterProvider = - ResourcePainterProvider.stateful(basePath, svgLoader) + ResourcePainterProvider.stateful(basePath, svgLoader, LocalIconData.current) @Composable fun externalLink( svgLoader: SvgLoader, - basePath: String = "icons/intui/externalLink.svg", + basePath: String = "expui/ide/externalLink.svg", ): PainterProvider = - ResourcePainterProvider.stateful(basePath, svgLoader) + ResourcePainterProvider.stateful(basePath, svgLoader, LocalIconData.current) } } @@ -152,8 +153,8 @@ data class IntUiLinkTextStyles( @Composable fun light( - normal: TextStyle = IntUiTheme.defaultLightTextStyle.copy(textDecoration = TextDecoration.Underline), - disabled: TextStyle = IntUiTheme.defaultLightTextStyle, + normal: TextStyle = IntUiTheme.defaultTextStyle.copy(textDecoration = TextDecoration.Underline), + disabled: TextStyle = IntUiTheme.defaultTextStyle, focused: TextStyle = normal, pressed: TextStyle = normal, hovered: TextStyle = normal, @@ -162,8 +163,8 @@ data class IntUiLinkTextStyles( @Composable fun dark( - normal: TextStyle = IntUiTheme.defaultDarkTextStyle.copy(textDecoration = TextDecoration.Underline), - disabled: TextStyle = IntUiTheme.defaultDarkTextStyle, + normal: TextStyle = IntUiTheme.defaultTextStyle.copy(textDecoration = TextDecoration.Underline), + disabled: TextStyle = IntUiTheme.defaultTextStyle, focused: TextStyle = normal, pressed: TextStyle = normal, hovered: TextStyle = normal, diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt index 9252303fa..670784e6c 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.MenuItemState import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme @@ -212,9 +213,9 @@ data class IntUiMenuIcons( @Composable fun submenuChevron( svgLoader: SvgLoader, - basePath: String = "icons/intui/chevronRight.svg", + basePath: String = "expui/general/chevronRight.svg", ): PainterProvider = - ResourcePainterProvider.stateful(basePath, svgLoader) + ResourcePainterProvider.stateful(basePath, svgLoader, LocalIconData.current) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt index eb109a54b..3622b9361 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt @@ -6,11 +6,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.RadioButtonState import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme -import org.jetbrains.jewel.intui.standalone.IntUiTheme import org.jetbrains.jewel.styling.PainterProvider import org.jetbrains.jewel.styling.RadioButtonColors import org.jetbrains.jewel.styling.RadioButtonIcons @@ -32,7 +32,7 @@ data class IntUiRadioButtonStyle( svgLoader: SvgLoader, colors: IntUiRadioButtonColors = IntUiRadioButtonColors.light(), metrics: IntUiRadioButtonMetrics = IntUiRadioButtonMetrics(), - icons: IntUiRadioButtonIcons = intUiRadioButtonIcons(svgLoader), + icons: IntUiRadioButtonIcons = IntUiRadioButtonIcons.light(svgLoader), ) = IntUiRadioButtonStyle(colors, metrics, icons) @Composable @@ -40,7 +40,7 @@ data class IntUiRadioButtonStyle( svgLoader: SvgLoader, colors: IntUiRadioButtonColors = IntUiRadioButtonColors.dark(), metrics: IntUiRadioButtonMetrics = IntUiRadioButtonMetrics(), - icons: IntUiRadioButtonIcons = intUiRadioButtonIcons(svgLoader), + icons: IntUiRadioButtonIcons = IntUiRadioButtonIcons.dark(svgLoader), ) = IntUiRadioButtonStyle(colors, metrics, icons) } } @@ -59,12 +59,12 @@ data class IntUiRadioButtonColors( @Composable fun light( - content: Color = IntUiTheme.defaultLightTextStyle.color, - contentHovered: Color = IntUiLightTheme.colors.grey(8), - contentDisabled: Color = content, + content: Color = Color.Unspecified, + contentHovered: Color = content, + contentDisabled: Color = IntUiLightTheme.colors.grey(8), contentSelected: Color = content, contentSelectedHovered: Color = content, - contentSelectedDisabled: Color = content, + contentSelectedDisabled: Color = contentDisabled, ) = IntUiRadioButtonColors( content, contentHovered, @@ -76,12 +76,12 @@ data class IntUiRadioButtonColors( @Composable fun dark( - content: Color = IntUiTheme.defaultDarkTextStyle.color, - contentHovered: Color = IntUiDarkTheme.colors.grey(8), - contentDisabled: Color = content, + content: Color = Color.Unspecified, + contentHovered: Color = content, + contentDisabled: Color = IntUiDarkTheme.colors.grey(8), contentSelected: Color = content, contentSelectedHovered: Color = content, - contentSelectedDisabled: Color = content, + contentSelectedDisabled: Color = contentDisabled, ) = IntUiRadioButtonColors( content, contentHovered, @@ -109,14 +109,26 @@ data class IntUiRadioButtonIcons( @Composable fun radioButton( svgLoader: SvgLoader, - basePath: String = "icons/intui/radio.svg", + basePath: String = "com/intellij/ide/ui/laf/icons/intellij/radio.svg", ): PainterProvider = - ResourcePainterProvider.stateful(basePath, svgLoader) + ResourcePainterProvider.stateful(basePath, svgLoader, LocalIconData.current) + + @Composable + fun light( + svgLoader: SvgLoader, + radioButton: PainterProvider = radioButton( + svgLoader, + "com/intellij/ide/ui/laf/icons/intellij/radio.svg", + ), + ) = IntUiRadioButtonIcons(radioButton) + + @Composable + fun dark( + svgLoader: SvgLoader, + radioButton: PainterProvider = radioButton( + svgLoader, + "com/intellij/ide/ui/laf/icons/darcula/radio.svg", + ), + ) = IntUiRadioButtonIcons(radioButton) } } - -@Composable -fun intUiRadioButtonIcons( - svgLoader: SvgLoader, - radioButton: PainterProvider = IntUiRadioButtonIcons.radioButton(svgLoader), -) = IntUiRadioButtonIcons(radioButton) diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt index 0d6d7b56b..5f2e10fd6 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiScrollbarStyling.kt @@ -49,14 +49,14 @@ data class IntUiScrollbarColors( @Composable fun light( - thumbBackground: Color = Color(0xFFD9D9D9), - thumbBackgroundHovered: Color = Color(0xFF7B7C7D), + thumbBackground: Color = Color(0x33000000), + thumbBackgroundHovered: Color = Color(0x80000000), ) = IntUiScrollbarColors(thumbBackground, thumbBackgroundHovered) @Composable fun dark( - thumbBackground: Color = Color(0xFF48494B), - thumbBackgroundHovered: Color = Color(0xFF595A5C), + thumbBackground: Color = Color(0x59808080), + thumbBackgroundHovered: Color = Color(0x8C808080), ) = IntUiScrollbarColors(thumbBackground, thumbBackgroundHovered) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt index 67393d019..f75dce47f 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTabStyling.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.jetbrains.jewel.ButtonState +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme @@ -139,7 +140,7 @@ data class IntUiTabColors( backgroundPressed: Color = backgroundHovered, backgroundSelected: Color = background, backgroundDisabled: Color = background, - content: Color = IntUiDarkTheme.colors.grey(12), + content: Color = Color.Unspecified, contentHovered: Color = content, contentDisabled: Color = content, contentPressed: Color = content, @@ -182,7 +183,7 @@ data class IntUiTabColors( backgroundPressed: Color = background, backgroundSelected: Color = background, backgroundDisabled: Color = background, - content: Color = IntUiLightTheme.colors.grey(3), + content: Color = Color.Unspecified, contentHovered: Color = content, contentDisabled: Color = content, contentPressed: Color = content, @@ -223,7 +224,7 @@ data class IntUiTabColors( backgroundSelected: Color = background, backgroundDisabled: Color = background, - content: Color = IntUiDarkTheme.colors.grey(12), + content: Color = Color.Unspecified, contentHovered: Color = content, contentDisabled: Color = content, contentPressed: Color = content, @@ -353,9 +354,9 @@ data class IntUiTabIcons( @Composable fun close( svgLoader: SvgLoader, - basePath: String = "icons/intui/closeSmall.svg", + basePath: String = "expui/general/closeSmall.svg", ): PainterProvider = - ResourcePainterProvider.stateful(basePath, svgLoader) + ResourcePainterProvider.stateful(basePath, svgLoader, LocalIconData.current) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextAreaStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextAreaStyling.kt index 030fde99a..ba09fea98 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextAreaStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextAreaStyling.kt @@ -30,14 +30,14 @@ data class IntUiTextAreaStyle( fun light( colors: IntUiTextAreaColors = IntUiTextAreaColors.light(), metrics: IntUiTextAreaMetrics = IntUiTextAreaMetrics(), - textStyle: TextStyle = IntUiTheme.defaultLightTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, ) = IntUiTextAreaStyle(colors, metrics, textStyle) @Composable fun dark( colors: IntUiTextAreaColors = IntUiTextAreaColors.dark(), metrics: IntUiTextAreaMetrics = IntUiTextAreaMetrics(), - textStyle: TextStyle = IntUiTheme.defaultDarkTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, ) = IntUiTextAreaStyle(colors, metrics, textStyle) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextFieldStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextFieldStyling.kt index c27349b67..f4e7c98d6 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextFieldStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTextFieldStyling.kt @@ -30,14 +30,14 @@ data class IntUiTextFieldStyle( fun light( colors: IntUiTextFieldColors = IntUiTextFieldColors.light(), metrics: IntUiTextFieldMetrics = IntUiTextFieldMetrics(), - textStyle: TextStyle = IntUiTheme.defaultLightTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, ) = IntUiTextFieldStyle(colors, metrics, textStyle) @Composable fun dark( colors: IntUiTextFieldColors = IntUiTextFieldColors.dark(), metrics: IntUiTextFieldMetrics = IntUiTextFieldMetrics(), - textStyle: TextStyle = IntUiTheme.defaultDarkTextStyle, + textStyle: TextStyle = IntUiTheme.defaultTextStyle, ) = IntUiTextFieldStyle(colors, metrics, textStyle) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt new file mode 100644 index 000000000..91fa66728 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt @@ -0,0 +1,81 @@ +package org.jetbrains.jewel.intui.standalone.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme +import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.styling.TooltipColors +import org.jetbrains.jewel.styling.TooltipMetrics +import org.jetbrains.jewel.styling.TooltipStyle +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +@Stable +data class IntUiTooltipStyle( + override val colors: IntUiTooltipColors, + override val metrics: IntUiTooltipMetrics, +) : TooltipStyle { + + companion object { + + @Composable + fun light( + intUiTooltipColors: IntUiTooltipColors = IntUiTooltipColors.light(), + intUiTooltipMetrics: IntUiTooltipMetrics = IntUiTooltipMetrics(), + ): IntUiTooltipStyle = IntUiTooltipStyle( + colors = intUiTooltipColors, + metrics = intUiTooltipMetrics, + ) + + @Composable + fun dark( + intUiTooltipColors: IntUiTooltipColors = IntUiTooltipColors.dark(), + intUiTooltipMetrics: IntUiTooltipMetrics = IntUiTooltipMetrics(), + ): IntUiTooltipStyle = IntUiTooltipStyle( + colors = intUiTooltipColors, + metrics = intUiTooltipMetrics, + ) + } +} + +@Stable +data class IntUiTooltipColors( + override val content: Color, + override val background: Color, + override val border: Color, + override val shadow: Color, +) : TooltipColors { + + companion object { + + @Composable + fun light( + contentColor: Color = IntUiLightTheme.colors.grey(12), + backgroundColor: Color = IntUiLightTheme.colors.grey(2), + borderColor: Color = backgroundColor, + shadow: Color = Color(0x78919191), // Not a palette color + ) = IntUiTooltipColors(contentColor, backgroundColor, borderColor, shadow) + + @Composable + fun dark( + contentColor: Color = IntUiDarkTheme.colors.grey(12), + backgroundColor: Color = IntUiDarkTheme.colors.grey(2), + shadow: Color = Color(0x66000000), // Not a palette color + borderColor: Color = IntUiDarkTheme.colors.grey(3), + ) = IntUiTooltipColors(contentColor, backgroundColor, borderColor, shadow) + } +} + +@Stable +data class IntUiTooltipMetrics( + override val contentPadding: PaddingValues = PaddingValues(vertical = 9.dp, horizontal = 12.dp), + override val showDelay: Duration = 0.milliseconds, + override val cornerSize: CornerSize = CornerSize(5.dp), + override val borderWidth: Dp = 1.dp, + override val shadowSize: Dp = 12.dp, +) : TooltipMetrics diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBox.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBox.svg new file mode 100644 index 000000000..040fba751 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBox.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxDisabled.svg new file mode 100644 index 000000000..d8fd6c72e --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxDisabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxFocused.svg new file mode 100644 index 000000000..ace309292 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxFocused.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelected.svg new file mode 100644 index 000000000..125303ced --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelectedDisabled.svg new file mode 100644 index 000000000..3fa19a323 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelectedDisabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelectedFocused.svg new file mode 100644 index 000000000..61522b37b --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxIndeterminateSelectedFocused.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelected.svg new file mode 100644 index 000000000..dd29faace --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelectedDisabled.svg new file mode 100644 index 000000000..64f901f11 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelectedDisabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelectedFocused.svg new file mode 100644 index 000000000..2160bc789 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkBoxSelectedFocused.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmark.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmark.svg new file mode 100644 index 000000000..b00b29aed --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmarkDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmarkDisabled.svg new file mode 100644 index 000000000..67a059a28 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmarkDisabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmarkSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmarkSelected.svg new file mode 100644 index 000000000..04db58fda --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/checkmarkSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radio.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radio.svg new file mode 100644 index 000000000..9e892f403 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioDisabled.svg new file mode 100644 index 000000000..6fcd561f3 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioDisabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioFocused.svg new file mode 100644 index 000000000..62dd358dc --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioFocused.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelected.svg new file mode 100644 index 000000000..d1f5370b0 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelectedDisabled.svg new file mode 100644 index 000000000..9d5c288a1 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelectedDisabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelectedFocused.svg new file mode 100644 index 000000000..dc261e997 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/darcula/radioSelectedFocused.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBox.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBox.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBox.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBox.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxDisabled.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxFocused.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxIndeterminateSelected.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelected.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxIndeterminateSelected.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxIndeterminateSelectedDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedDisabled.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxIndeterminateSelectedDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxIndeterminateSelectedFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedFocused.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxIndeterminateSelectedFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxSelected.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelected.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxSelected.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxSelectedDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedDisabled.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxSelectedDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxSelectedFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedFocused.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkBoxSelectedFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmark.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmark.svg new file mode 100644 index 000000000..1b4e68e7d --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkDisabled.svg new file mode 100644 index 000000000..3bb339b91 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkDisabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkDisabled_dark.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkDisabled_dark.svg new file mode 100644 index 000000000..76e421895 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkDisabled_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkSelected.svg new file mode 100644 index 000000000..496154505 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkSelected.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkSelected_dark.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkSelected_dark.svg new file mode 100644 index 000000000..c0a68987a --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmarkSelected_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmark_dark.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmark_dark.svg new file mode 100644 index 000000000..7ead71f9a --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/checkmark_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radio.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radio.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radio.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radio.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioDisabled.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioFocused.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelected.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioSelected.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelected.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioSelected.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioSelectedDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedDisabled.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioSelectedDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioSelectedFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedFocused.svg rename to int-ui/int-ui-standalone/src/main/resources/com/intellij/ide/ui/laf/icons/intellij/radioSelectedFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/add.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/add.svg new file mode 100644 index 000000000..57eaaf561 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/add_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/add_dark.svg new file mode 100644 index 000000000..397e9321b --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/add_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronDown.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDown.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronDown.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDown.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLarge.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLarge.svg new file mode 100644 index 000000000..f8a847563 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLarge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLargeWhite.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLargeWhite.svg new file mode 100644 index 000000000..7ec9421a4 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLargeWhite.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLarge_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLarge_dark.svg new file mode 100644 index 000000000..70a43560d --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDownLarge_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronDown_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDown_dark.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronDown_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/chevronDown_dark.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronLeft.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronLeft.svg new file mode 100644 index 000000000..c6c4fa041 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronLeft.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronLeft_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronLeft_dark.svg new file mode 100644 index 000000000..d4284d234 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronLeft_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronRight.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronRight.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronRight.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/chevronRight.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronRight_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronRight_dark.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/chevronRight_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/chevronRight_dark.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUp.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUp.svg new file mode 100644 index 000000000..e40a745d7 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUpLarge.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUpLarge.svg new file mode 100644 index 000000000..c681a5d26 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUpLarge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUpLarge_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUpLarge_dark.svg new file mode 100644 index 000000000..bb98e4add --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUpLarge_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUp_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUp_dark.svg new file mode 100644 index 000000000..5ecca2a0c --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/chevronUp_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/close.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/close.svg new file mode 100644 index 000000000..9027624dd --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallSelectedHovered_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmall.svg similarity index 91% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallSelectedHovered_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmall.svg index e03d4bd63..c375b9331 100644 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallSelectedHovered_dark.svg +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmall.svg @@ -1,5 +1,4 @@ - - + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmallHovered.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmallHovered.svg new file mode 100644 index 000000000..cc358ef93 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmallHovered.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallHovered_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmallHovered_dark.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallHovered_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmallHovered_dark.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmall_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmall_dark.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmall_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/expui/general/closeSmall_dark.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/general/close_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/general/close_dark.svg new file mode 100644 index 000000000..22bb872bc --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/general/close_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/ide/externalLink.svg b/int-ui/int-ui-standalone/src/main/resources/expui/ide/externalLink.svg new file mode 100644 index 000000000..24846b20d --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/ide/externalLink.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/expui/ide/externalLink_dark.svg b/int-ui/int-ui-standalone/src/main/resources/expui/ide/externalLink_dark.svg new file mode 100644 index 000000000..de3c1480e --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/expui/ide/externalLink_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/add.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/add.svg deleted file mode 100644 index 2c9233bf0..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/add.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/add_dark.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/add_dark.svg deleted file mode 100644 index 548e6afd4..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/add_dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxFocused_dark.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxFocused_dark.svg deleted file mode 100644 index ac4576f25..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxFocused_dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBox_dark.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBox_dark.svg deleted file mode 100644 index 593579765..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBox_dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmall.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmall.svg deleted file mode 100644 index 75b624b42..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmall.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallHovered.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallHovered.svg deleted file mode 100644 index a84bc61a4..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallHovered.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallSelectedHovered.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallSelectedHovered.svg deleted file mode 100644 index a84bc61a4..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/closeSmallSelectedHovered.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/externalLink.svg b/int-ui/int-ui-standalone/src/main/resources/icons/intui/externalLink.svg deleted file mode 100644 index 3f87f82c5..000000000 --- a/int-ui/int-ui-standalone/src/main/resources/icons/intui/externalLink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg new file mode 100644 index 000000000..f6a69e272 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg @@ -0,0 +1,3 @@ + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxDisabled_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxDisabled_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg new file mode 100644 index 000000000..cd844a2a4 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg new file mode 100644 index 000000000..8615c4cc4 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelected_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelected.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelected_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelected.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedDisabled_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedDisabled_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedFocused_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxIndeterminateSelectedFocused_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelected_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelected.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelected_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelected.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedDisabled_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedDisabled_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedFocused_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/checkBoxSelectedFocused_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg new file mode 100644 index 000000000..0a87d5a3d --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radio_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radio.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radio_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radio.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioDisabled_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioDisabled_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioFocused_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioFocused_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg new file mode 100644 index 000000000..2e303ca37 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelected_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelected.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelected_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelected.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedDisabled_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedDisabled.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedDisabled_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedDisabled.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedFocused_dark.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedFocused.svg similarity index 100% rename from int-ui/int-ui-standalone/src/main/resources/icons/intui/radioSelectedFocused_dark.svg rename to int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedFocused.svg diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg new file mode 100644 index 000000000..c59b72429 --- /dev/null +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/ide-plugin/build.gradle.kts b/samples/ide-plugin/build.gradle.kts index 3a26f937e..839531cb0 100644 --- a/samples/ide-plugin/build.gradle.kts +++ b/samples/ide-plugin/build.gradle.kts @@ -1,13 +1,20 @@ +import SupportedIJVersion.* + plugins { jewel alias(libs.plugins.composeDesktop) alias(libs.plugins.ideaGradlePlugin) + `android-studio-releases-generator` } intellij { pluginName.set("Jewel Demo") plugins.set(listOf("org.jetbrains.kotlin")) - version.set("2023.2.1") + val versionRaw = when (supportedIJVersion()) { + IJ_232 -> libs.versions.idea232.get() + IJ_233 -> libs.versions.idea233.get() + } + version.set(versionRaw) } // TODO remove this once the IJ Gradle plugin fixes their repositories bug @@ -21,5 +28,19 @@ repositories { } dependencies { - implementation(projects.ideLafBridge) + implementation(projects.ideLafBridge) { + exclude(group = "org.jetbrains.kotlinx") + } + + implementation(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.kotlinx") + } +} + +tasks { + // We don't have any settings in the demo plugin + buildSearchableOptions { + enabled = false + } } diff --git a/samples/ide-plugin/src/main/kotlin/icons/JewelIcons.kt b/samples/ide-plugin/src/main/kotlin/icons/JewelIcons.kt new file mode 100644 index 000000000..5019bb2b4 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/icons/JewelIcons.kt @@ -0,0 +1,8 @@ +package icons + +import com.intellij.openapi.util.IconLoader + +object JewelIcons { + + @JvmField val ToolWindowIcon = IconLoader.getIcon("/icons/jewel-tool-window.svg", javaClass) +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt new file mode 100644 index 000000000..41d35cae2 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt @@ -0,0 +1,209 @@ +package org.jetbrains.jewel.samples.ideplugin + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.ResourceLoader +import androidx.compose.ui.unit.dp +import com.intellij.openapi.components.service +import com.intellij.ui.JBColor +import org.jetbrains.jewel.CheckboxRow +import org.jetbrains.jewel.CircularProgressIndicator +import org.jetbrains.jewel.CircularProgressIndicatorBig +import org.jetbrains.jewel.DefaultButton +import org.jetbrains.jewel.Icon +import org.jetbrains.jewel.IconButton +import org.jetbrains.jewel.LazyTree +import org.jetbrains.jewel.LocalResourceLoader +import org.jetbrains.jewel.OutlinedButton +import org.jetbrains.jewel.RadioButtonRow +import org.jetbrains.jewel.Text +import org.jetbrains.jewel.TextField +import org.jetbrains.jewel.Tooltip +import org.jetbrains.jewel.bridge.SwingBridgeService +import org.jetbrains.jewel.bridge.SwingBridgeTheme +import org.jetbrains.jewel.bridge.retrieveStatelessIcon +import org.jetbrains.jewel.bridge.toComposeColor +import org.jetbrains.jewel.foundation.tree.buildTree +import org.jetbrains.jewel.intui.standalone.IntUiTheme + +@Composable +internal fun ComponentShowcaseTab() { + SwingBridgeTheme { + val resourceLoader = LocalResourceLoader.current + val bgColor by remember(IntUiTheme.isDark) { mutableStateOf(JBColor.PanelBackground.toComposeColor()) } + + val scrollState = rememberScrollState() + Row( + modifier = Modifier + .fillMaxSize() + .background(bgColor) + .verticalScroll(scrollState) + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + ColumnOne(resourceLoader) + ColumnTwo(resourceLoader) + } + } +} + +@Composable +private fun RowScope.ColumnOne(resourceLoader: ResourceLoader) { + Column( + Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text("Here is a selection of our finest components:") + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + var clicks1 by remember { mutableStateOf(0) } + OutlinedButton({ clicks1++ }) { + Text("Outlined: $clicks1") + } + OutlinedButton({ }, enabled = false) { + Text("Outlined") + } + + var clicks2 by remember { mutableStateOf(0) } + DefaultButton({ clicks2++ }) { + Text("Default: $clicks2") + } + DefaultButton({ }, enabled = false) { + Text("Default") + } + } + + var textFieldValue by remember { mutableStateOf("") } + TextField( + value = textFieldValue, + onValueChange = { textFieldValue = it }, + placeholder = { Text("Write something...") }, + modifier = Modifier.width(200.dp), + ) + + var checked by remember { mutableStateOf(false) } + CheckboxRow( + checked = checked, + onCheckedChange = { checked = it }, + resourceLoader = resourceLoader, + ) { + Text("Hello, I am a themed checkbox") + } + + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + var index by remember { mutableStateOf(0) } + RadioButtonRow(selected = index == 0, resourceLoader, onClick = { index = 0 }) { + Text("I am number one") + } + RadioButtonRow(selected = index == 1, resourceLoader, onClick = { index = 1 }) { + Text("Sad second") + } + } + + val svgLoader = service().svgLoader + Row { + val painterProvider = retrieveStatelessIcon("actions/close.svg", svgLoader, IntUiTheme.iconData) + val painter by painterProvider.getPainter(resourceLoader) + Icon(painter = painter, modifier = Modifier.border(1.dp, Color.Magenta), contentDescription = "An icon") + } + + Row { + IconButton(onClick = { }) { + val painterProvider = retrieveStatelessIcon("actions/close.svg", svgLoader, IntUiTheme.iconData) + val painter by painterProvider.getPainter(resourceLoader) + Icon(painter = painter, contentDescription = "An icon") + } + } + + Row { + Text("Circular progress small: ") + CircularProgressIndicator(svgLoader) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Circular progress big: ") + CircularProgressIndicatorBig(svgLoader) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Tooltip(tooltip = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + val painterProvider = + retrieveStatelessIcon("general/showInfos.svg", svgLoader, IntUiTheme.iconData) + val painter by painterProvider.getPainter(resourceLoader) + Icon(painter = painter, contentDescription = null) + + Text("This is a tooltip") + } + }) { + Text( + modifier = Modifier.border(1.dp, IntUiTheme.globalColors.borders.normal) + .padding(12.dp, 8.dp), + text = "Hover Me!", + ) + } + } + } +} + +@Composable +private fun RowScope.ColumnTwo(resourceLoader: ResourceLoader) { + Column( + Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + val tree = remember { + buildTree { + addNode("root 1") { + addLeaf("leaf 1") + addLeaf("leaf 2") + } + addNode("root 2") { + addLeaf("leaf 1") + addNode("node 1") { + addLeaf("leaf 1") + addLeaf("leaf 2") + } + } + addNode("root 3") { + addLeaf("leaf 1") + addLeaf("leaf 2") + } + } + } + LazyTree( + tree = tree, + resourceLoader = resourceLoader, + modifier = Modifier.height(200.dp).fillMaxWidth(), + onElementClick = {}, + onElementDoubleClick = {}, + ) { element -> + Box(Modifier.fillMaxWidth()) { + Text(element.data, Modifier.padding(2.dp)) + } + } + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindow.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindow.kt deleted file mode 100644 index f4535ce8b..000000000 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindow.kt +++ /dev/null @@ -1,181 +0,0 @@ -package org.jetbrains.jewel.samples.ideplugin - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.ResourceLoader -import androidx.compose.ui.unit.dp -import com.intellij.openapi.components.service -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -import com.intellij.ui.JBColor -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.jetbrains.jewel.CheckboxRow -import org.jetbrains.jewel.DefaultButton -import org.jetbrains.jewel.ExperimentalJewelApi -import org.jetbrains.jewel.Icon -import org.jetbrains.jewel.LazyTree -import org.jetbrains.jewel.LocalResourceLoader -import org.jetbrains.jewel.OutlinedButton -import org.jetbrains.jewel.RadioButtonRow -import org.jetbrains.jewel.Text -import org.jetbrains.jewel.TextField -import org.jetbrains.jewel.bridge.SwingBridgeService -import org.jetbrains.jewel.bridge.SwingBridgeTheme -import org.jetbrains.jewel.bridge.addComposeTab -import org.jetbrains.jewel.bridge.retrieveStatelessIcon -import org.jetbrains.jewel.bridge.toComposeColor -import org.jetbrains.jewel.foundation.tree.buildTree -import org.jetbrains.jewel.intui.standalone.IntUiTheme - -@ExperimentalCoroutinesApi -internal class JewelDemoToolWindow : ToolWindowFactory, DumbAware { - - @OptIn(ExperimentalJewelApi::class) - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - toolWindow.addComposeTab("Jewel") { - SwingBridgeTheme { - val resourceLoader = LocalResourceLoader.current - val bgColor by remember(IntUiTheme.isDark) { mutableStateOf(JBColor.PanelBackground.toComposeColor()) } - - val scrollState = rememberScrollState() - Row( - modifier = Modifier - .fillMaxSize() - .background(bgColor) - .verticalScroll(scrollState) - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - ColumnOne(resourceLoader) - ColumnTwo(resourceLoader) - } - } - } - } - - @Composable - private fun RowScope.ColumnOne(resourceLoader: ResourceLoader) { - Column( - Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text("Here is a selection of our finest components:") - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - var clicks1 by remember { mutableStateOf(0) } - OutlinedButton({ clicks1++ }) { - Text("Outlined: $clicks1") - } - OutlinedButton({ }, enabled = false) { - Text("Outlined") - } - - var clicks2 by remember { mutableStateOf(0) } - DefaultButton({ clicks2++ }) { - Text("Default: $clicks2") - } - DefaultButton({ }, enabled = false) { - Text("Default") - } - } - - var textFieldValue by remember { mutableStateOf("") } - TextField( - value = textFieldValue, - onValueChange = { textFieldValue = it }, - placeholder = { Text("Write something...") }, - modifier = Modifier.width(200.dp), - ) - - var checked by remember { mutableStateOf(false) } - CheckboxRow( - checked = checked, - onCheckedChange = { checked = it }, - resourceLoader = resourceLoader, - ) { - Text("Hello, I am a themed checkbox") - } - - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - var index by remember { mutableStateOf(0) } - RadioButtonRow(selected = index == 0, resourceLoader, onClick = { index = 0 }) { - Text("I am number one") - } - RadioButtonRow(selected = index == 1, resourceLoader, onClick = { index = 1 }) { - Text("Sad second") - } - } - - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - val svgLoader = service().svgLoader - val painterProvider = retrieveStatelessIcon("actions/close.svg", svgLoader, IntUiTheme.iconData) - val painter by painterProvider.getPainter(resourceLoader) - Icon(painter = painter, modifier = Modifier.border(1.dp, Color.Magenta), contentDescription = "An icon") - } - } - } - - @Composable - private fun RowScope.ColumnTwo(resourceLoader: ResourceLoader) { - Column( - Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - val tree = remember { - buildTree { - addNode("root 1") { - addLeaf("leaf 1") - addLeaf("leaf 2") - } - addNode("root 2") { - addLeaf("leaf 1") - addNode("node 1") { - addLeaf("leaf 1") - addLeaf("leaf 2") - } - } - addNode("root 3") { - addLeaf("leaf 1") - addLeaf("leaf 2") - } - } - } - LazyTree( - tree = tree, - resourceLoader = resourceLoader, - modifier = Modifier.height(200.dp).fillMaxWidth(), - onElementClick = {}, - onElementDoubleClick = {}, - ) { element -> - Box(Modifier.fillMaxWidth()) { - Text(element.data, Modifier.padding(2.dp)) - } - } - } - } -} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt new file mode 100644 index 000000000..4cbafee94 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt @@ -0,0 +1,52 @@ +package org.jetbrains.jewel.samples.ideplugin + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import org.jetbrains.jewel.bridge.addComposeTab +import org.jetbrains.jewel.samples.ideplugin.releasessample.ReleasesSampleCompose +import org.jetbrains.jewel.samples.ideplugin.releasessample.ReleasesSamplePanel + +@ExperimentalCoroutinesApi +internal class JewelDemoToolWindowFactory : ToolWindowFactory, DumbAware { + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + toolWindow.addComposeTab("Components") { + ComponentShowcaseTab() + } + + addSwingTab(toolWindow) + + toolWindow.addComposeTab("Compose Sample") { + ReleasesSampleCompose(project) + } + } + + private fun addSwingTab(toolWindow: ToolWindow) { + val manager = toolWindow.contentManager + val tabContent = + manager.factory.createContent( + ReleasesSamplePanel(toolWindow.disposable.createCoroutineScope()), + "Swing Sample", + true, + ) + tabContent.isCloseable = false + manager.addContent(tabContent) + } +} + +@Suppress("InjectDispatcher") // This is likely wrong anyway, it's only for the demo +private fun Disposable.createCoroutineScope(): CoroutineScope { + val job = SupervisorJob() + val scopeDisposable = Disposable { job.cancel("Disposing") } + Disposer.register(this, scopeDisposable) + return CoroutineScope(job + Dispatchers.Default) +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ApiLevelIndication.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ApiLevelIndication.kt new file mode 100644 index 000000000..998444710 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ApiLevelIndication.kt @@ -0,0 +1,41 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.openapi.ui.GraphicsConfig +import com.intellij.ui.paint.RectanglePainter +import com.intellij.ui.scale.JBUIScale +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import java.awt.Graphics +import java.awt.Graphics2D +import javax.swing.JLabel + +internal class ApiLevelIndication(apiLevel: Int) : JLabel("API level $apiLevel") { + + init { + border = JBUI.Borders.empty(2, 4) + foreground = ReleaseChannel.Other.foreground + isOpaque = false + font = JBFont.medium() + } + + override fun paint(g: Graphics?) { + with(g as Graphics2D) { + val graphicsConfig = GraphicsConfig(this) + graphicsConfig.setupRoundedBorderAntialiasing() + + RectanglePainter.paint( + this, + x, + y, + width, + height - y, + JBUIScale.scale(8), + ReleaseChannel.Other.background, + null, + ) + + graphicsConfig.restore() + } + super.paint(g) + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ChannelIndication.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ChannelIndication.kt new file mode 100644 index 000000000..d142751d7 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ChannelIndication.kt @@ -0,0 +1,32 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.openapi.ui.GraphicsConfig +import com.intellij.ui.paint.RectanglePainter +import com.intellij.ui.scale.JBUIScale +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import java.awt.Graphics +import java.awt.Graphics2D +import javax.swing.JLabel + +internal class ChannelIndication(private val channel: ReleaseChannel) : JLabel(channel.name.lowercase()) { + + init { + border = JBUI.Borders.empty(2, 4) + foreground = channel.foreground // TODO why does this not work UGGHHH + isOpaque = false + font = JBFont.medium() + } + + override fun paint(g: Graphics?) { + with(g as Graphics2D) { + val graphicsConfig = GraphicsConfig(this) + graphicsConfig.setupRoundedBorderAntialiasing() + + RectanglePainter.paint(this, x, y, width, height - y, JBUIScale.scale(8), channel.background, null) + + graphicsConfig.restore() + } + super.paint(g) + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ContentItem.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ContentItem.kt new file mode 100644 index 000000000..cc269f7ba --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ContentItem.kt @@ -0,0 +1,56 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import kotlinx.datetime.LocalDate +import org.jetbrains.annotations.Nls + +sealed class ContentItem { + + @get:Nls abstract val displayText: String + abstract val imagePath: String? + abstract val versionName: String + abstract val releaseDate: LocalDate? + abstract val key: Any + + data class AndroidStudio( + @Nls override val displayText: String, + override val imagePath: String?, + override val versionName: String, + val build: String, + val platformBuild: String, + val platformVersion: String, + val channel: ReleaseChannel, + override val releaseDate: LocalDate?, + override val key: Any = build, + ) : ContentItem() + + data class AndroidRelease( + @Nls override val displayText: String, + override val imagePath: String?, + override val versionName: String, + val codename: String?, + val apiLevel: Int, + override val releaseDate: LocalDate?, + override val key: Any = releaseDate ?: displayText, + ) : ContentItem() +} + +fun ContentItem.matches(text: String): Boolean { + if (displayText.contains(text, ignoreCase = true)) return true + if (versionName.contains(text, ignoreCase = true)) return true + + when (this) { + is ContentItem.AndroidStudio -> { + if (build.contains(text, ignoreCase = true)) return true + if (channel.name.contains(text, ignoreCase = true)) return true + if (platformBuild.contains(text, ignoreCase = true)) return true + if (platformVersion.contains(text, ignoreCase = true)) return true + } + + is ContentItem.AndroidRelease -> { + if (codename?.contains(text, ignoreCase = true) == true) return true + if (this.apiLevel.toString().contains(text, ignoreCase = true)) return true + } + } + + return false +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ContentSource.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ContentSource.kt new file mode 100644 index 000000000..808cc6a1c --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ContentSource.kt @@ -0,0 +1,312 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import kotlinx.datetime.LocalDate + +abstract class ContentSource { + + abstract val items: List + + abstract val displayName: String + + fun isSameAs(other: ContentSource<*>): Boolean { + val thisComparable = getComparableSource() + val otherComparable = other.getComparableSource() + + return thisComparable == otherComparable + } + + private fun getComparableSource() = + when (this) { + is FilteredContentSource<*> -> original + else -> this + } +} + +data class FilteredContentSource( + override val items: List, + val original: ContentSource<*>, +) : ContentSource() { + + override val displayName: String + get() = original.displayName +} + +object AndroidReleases : ContentSource() { + + override val items = listOf( + ContentItem.AndroidRelease( + displayText = "Android 1.0", + imagePath = null, + versionName = "1.0", + codename = null, + apiLevel = 1, + releaseDate = LocalDate(2008, 9, 23), + ), + ContentItem.AndroidRelease( + displayText = "Android 1.1", + imagePath = null, + versionName = "1.1", + codename = "Petit Four", + apiLevel = 2, + releaseDate = LocalDate(2009, 2, 9), + ), + ContentItem.AndroidRelease( + displayText = "Android Cupcake", + imagePath = "/android-releases/cupcake.png", + versionName = "1.5", + codename = "Cupcake", + apiLevel = 3, + releaseDate = LocalDate(2009, 4, 27), + ), + ContentItem.AndroidRelease( + displayText = "Android Donut", + imagePath = "/android-releases/donut.png", + versionName = "1.6", + codename = "Donut", + apiLevel = 4, + releaseDate = LocalDate(2009, 9, 15), + ), + ContentItem.AndroidRelease( + displayText = "Android Eclair (2.0)", + imagePath = "/android-releases/eclair.png", + versionName = "2.0", + codename = "Eclair", + apiLevel = 5, + releaseDate = LocalDate(2009, 10, 27), + ), + ContentItem.AndroidRelease( + displayText = "Android Eclair (2.0.1)", + imagePath = "/android-releases/eclair.png", + versionName = "2.0.1", + codename = "Eclair", + apiLevel = 6, + releaseDate = LocalDate(2009, 12, 3), + ), + ContentItem.AndroidRelease( + displayText = "Android Eclair (2.1)", + imagePath = "/android-releases/eclair.png", + versionName = "2.1", + codename = "Eclair", + apiLevel = 7, + releaseDate = LocalDate(2010, 1, 11), + ), + ContentItem.AndroidRelease( + displayText = "Android Froyo", + imagePath = "/android-releases/froyo.png", + versionName = "2.2 – 2.2.3", + codename = "Froyo", + apiLevel = 8, + releaseDate = LocalDate(2010, 5, 20), + ), + ContentItem.AndroidRelease( + displayText = "Android Gingerbread (2.3 – 2.3.2)", + imagePath = "/android-releases/gingerbread.png", + versionName = "2.3 – 2.3.2", + codename = "Gingerbread", + apiLevel = 9, + releaseDate = LocalDate(2010, 12, 6), + ), + ContentItem.AndroidRelease( + displayText = "Android Gingerbread (2.3.3 – 2.3.7)", + imagePath = "/android-releases/gingerbread.png", + versionName = "2.3.3 – 2.3.7", + codename = "Gingerbread", + apiLevel = 10, + releaseDate = LocalDate(2011, 2, 9), + ), + ContentItem.AndroidRelease( + displayText = "Android Honeycomb (3.0)", + imagePath = "/android-releases/honeycomb.svg", + versionName = "3.0", + codename = "Honeycomb", + apiLevel = 11, + releaseDate = LocalDate(2011, 2, 22), + ), + ContentItem.AndroidRelease( + displayText = "Android Honeycomb (3.1)", + imagePath = "/android-releases/honeycomb.svg", + versionName = "3.1", + codename = "Honeycomb", + apiLevel = 12, + releaseDate = LocalDate(2011, 5, 10), + ), + ContentItem.AndroidRelease( + displayText = "Android Honeycomb (3.2 – 3.2.6)", + imagePath = "/android-releases/honeycomb.svg", + versionName = "3.2 – 3.2.6", + codename = "Honeycomb", + apiLevel = 13, + releaseDate = LocalDate(2011, 7, 15), + ), + ContentItem.AndroidRelease( + displayText = "Android Ice Cream Sandwich (4.0 – 4.0.2)", + imagePath = "/android-releases/ice-cream-sandwich.svg", + versionName = "4.0 – 4.0.2", + codename = "Ice Cream Sandwich", + apiLevel = 14, + releaseDate = LocalDate(2011, 10, 18), + ), + ContentItem.AndroidRelease( + displayText = "Android Ice Cream Sandwich (4.0.3 – 4.0.4)", + imagePath = "/android-releases/ice-cream-sandwich.svg", + versionName = "4.0.3 – 4.0.4", + codename = "Ice Cream Sandwich", + apiLevel = 15, + releaseDate = LocalDate(2011, 12, 16), + ), + ContentItem.AndroidRelease( + displayText = "Android Jelly Bean (4.1 – 4.1.2)", + imagePath = "/android-releases/jelly-bean.svg", + versionName = "4.1 – 4.1.2", + codename = "Jelly Bean", + apiLevel = 16, + releaseDate = LocalDate(2012, 7, 9), + ), + ContentItem.AndroidRelease( + displayText = "Android Jelly Bean (4.2 – 4.2.2)", + imagePath = "/android-releases/jelly-bean.svg", + versionName = "4.2 – 4.2.2", + codename = "Jelly Bean", + apiLevel = 17, + releaseDate = LocalDate(2012, 11, 13), + ), + ContentItem.AndroidRelease( + displayText = "Android Jelly Bean (4.3 – 4.3.1)", + imagePath = "/android-releases/jelly-bean.svg", + versionName = "4.3 – 4.3.1", + codename = "Jelly Bean", + apiLevel = 18, + releaseDate = LocalDate(2013, 7, 24), + ), + ContentItem.AndroidRelease( + displayText = "Android KitKat (4.4 – 4.4.4)", + imagePath = "/android-releases/kitkat.svg", + versionName = "4.4 – 4.4.4", + codename = "Key Lime Pie", + apiLevel = 19, + releaseDate = LocalDate(2013, 10, 31), + ), + ContentItem.AndroidRelease( + displayText = "Android KitKat (4.4W – 4.4W.2)", + imagePath = "/android-releases/kitkat.svg", + versionName = "4.4W – 4.4W.2", + codename = "Key Lime Pie", + apiLevel = 20, + releaseDate = LocalDate(2014, 6, 25), + ), + ContentItem.AndroidRelease( + displayText = "Android Lollipop (5.0 – 5.0.2)", + imagePath = "/android-releases/lollipop.svg", + versionName = "5.0 – 5.0.2", + codename = "Lemon Meringue Pie", + apiLevel = 21, + releaseDate = LocalDate(2014, 10, 4), + ), + ContentItem.AndroidRelease( + displayText = "Android Lollipop (5.1 – 5.1.1)", + imagePath = "/android-releases/lollipop.svg", + versionName = "5.1 – 5.1.1", + codename = "Lemon Meringue Pie", + apiLevel = 22, + releaseDate = LocalDate(2015, 3, 2), + ), + ContentItem.AndroidRelease( + displayText = "Android Marshmallow", + imagePath = "/android-releases/marshmallow.svg", + versionName = "6.0 – 6.0.1", + codename = "Macadamia Nut Cookie", + apiLevel = 23, + releaseDate = LocalDate(2015, 10, 2), + ), + ContentItem.AndroidRelease( + displayText = "Android Nougat (7.0)", + imagePath = "/android-releases/nougat.svg", + versionName = "7.0", + codename = "New York Cheesecake", + apiLevel = 24, + releaseDate = LocalDate(2016, 8, 22), + ), + ContentItem.AndroidRelease( + displayText = "Android Nougat (7.1 – 7.1.2)", + imagePath = "/android-releases/nougat.svg", + versionName = "7.1 – 7.1.2", + codename = "New York Cheesecake", + apiLevel = 25, + releaseDate = LocalDate(2016, 10, 4), + ), + ContentItem.AndroidRelease( + displayText = "Android Oreo (8.0)", + imagePath = "/android-releases/oreo.svg", + versionName = "8.0", + codename = "Oatmeal Cookie", + apiLevel = 26, + releaseDate = LocalDate(2017, 8, 21), + ), + ContentItem.AndroidRelease( + displayText = "Android Oreo (8.1)", + imagePath = "/android-releases/oreo.svg", + versionName = "8.1", + codename = "Oatmeal Cookie", + apiLevel = 27, + releaseDate = LocalDate(2017, 12, 5), + ), + ContentItem.AndroidRelease( + displayText = "Android Pie", + imagePath = "/android-releases/pie.svg", + versionName = "9", + codename = "Pistachio Ice Cream", + apiLevel = 28, + releaseDate = LocalDate(2018, 8, 6), + ), + ContentItem.AndroidRelease( + displayText = "Android 10", + imagePath = "/android-releases/10.svg", + versionName = "10", + codename = "Quince Tart", + apiLevel = 29, + releaseDate = LocalDate(2019, 9, 3), + ), + ContentItem.AndroidRelease( + displayText = "Android 11", + imagePath = "/android-releases/11.svg", + versionName = "11", + codename = "Red Velvet Cake", + apiLevel = 30, + releaseDate = LocalDate(2020, 9, 8), + ), + ContentItem.AndroidRelease( + displayText = "Android 12", + imagePath = "/android-releases/12.svg", + versionName = "12", + codename = "Snow Cone", + apiLevel = 31, + releaseDate = LocalDate(2021, 10, 4), + ), + ContentItem.AndroidRelease( + displayText = "Android 12L", + imagePath = "/android-releases/12.svg", + versionName = "12.1", + codename = "Snow Cone v2", + apiLevel = 32, + releaseDate = LocalDate(2022, 3, 7), + ), + ContentItem.AndroidRelease( + displayText = "Android 13", + imagePath = "/android-releases/13.svg", + versionName = "13", + codename = "Tiramisu", + apiLevel = 33, + releaseDate = LocalDate(2022, 8, 15), + ), + ContentItem.AndroidRelease( + displayText = "Android 14", + imagePath = "/android-releases/14.svg", + versionName = "14", + codename = "Upside Down Cake", + apiLevel = 33, + releaseDate = LocalDate(2023, 10, 4), + ), + ) + + override val displayName = "Android releases" +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/DetailsPanel.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/DetailsPanel.kt new file mode 100644 index 000000000..40fc46786 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/DetailsPanel.kt @@ -0,0 +1,131 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.ui.components.JBPanelWithEmptyText +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.scale.JBUIScale.scale +import com.intellij.ui.util.maximumHeight +import com.intellij.util.ImageLoader +import com.intellij.util.ui.ComponentWithEmptyText +import com.intellij.util.ui.ImageUtil +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import kotlinx.coroutines.CoroutineScope +import kotlinx.datetime.toJavaLocalDate +import java.awt.BorderLayout +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import javax.swing.ScrollPaneConstants + +internal class DetailsPanel(private val scope: CoroutineScope) : JBPanelWithEmptyText(BorderLayout()), ComponentWithEmptyText { + + fun display(contentItem: ContentItem?) { + removeAll() + + val content = when (contentItem) { + is ContentItem.AndroidRelease -> ItemDetailsPanel(contentItem, scope) + is ContentItem.AndroidStudio -> ItemDetailsPanel(contentItem, scope) + null -> return + } + add(content, BorderLayout.CENTER) + } +} + +private class ItemDetailsPanel( + contentItem: ContentItem, + scope: CoroutineScope, +) : BorderLayoutPanel() { + + private val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + + init { + val bufferedImage = contentItem.imagePath + ?.let { ImageLoader.loadFromResource(it, javaClass) } + ?.let { ImageUtil.toBufferedImage(it) } + + if (bufferedImage != null) { + val imageContainer = ImageComponent(scope).apply { + maximumHeight = scale(200) + image = bufferedImage + } + + addToTop(imageContainer) + } + + // Using the Kotlin DSL v2 to make this less painful + val mainContentPanel = panel { + commonContent(contentItem) + + when (contentItem) { + is ContentItem.AndroidRelease -> androidReleaseContent(contentItem) + is ContentItem.AndroidStudio -> androidStudioContent(contentItem) + } + } + mainContentPanel.border = JBUI.Borders.empty(12, 20) + + val scrollingContainer = + JBScrollPane( + mainContentPanel, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, + ) + + addToCenter(scrollingContainer) + } + + private fun Panel.commonContent(contentItem: ContentItem) { + row { + text(contentItem.displayText) + .let { + val releaseDate = contentItem.releaseDate + if (releaseDate != null) { + it.comment("Released on ${formatter.format(releaseDate.toJavaLocalDate())}") + } else { + it + } + } + .component.font = JBFont.h1() + }.bottomGap(BottomGap.MEDIUM) + } + + private fun Panel.androidReleaseContent(contentItem: ContentItem.AndroidRelease) { + row { + label("Codename:") + text(contentItem.codename ?: "N/A").bold() + } + row { + label("Version:") + text(contentItem.versionName).bold() + } + row { + label("API level:") + text(contentItem.apiLevel.toString()).bold() + } + } + + private fun Panel.androidStudioContent(contentItem: ContentItem.AndroidStudio) { + row { + label("Channel:") + text(contentItem.channel.name).bold() + } + row { + label("Version:") + text(contentItem.versionName).bold() + } + row { + label("IntelliJ Platform version:") + text(contentItem.platformVersion).bold() + } + row { + label("IntelliJ Platform build:") + text(contentItem.platformBuild).bold() + } + row { + label("Full build number:") + text(contentItem.build).bold() + } + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/FoilModifier.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/FoilModifier.kt new file mode 100644 index 000000000..82f0058cd --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/FoilModifier.kt @@ -0,0 +1,95 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asComposeRenderEffect +import androidx.compose.ui.graphics.graphicsLayer +import org.intellij.lang.annotations.Language +import org.jetbrains.skia.ImageFilter +import org.jetbrains.skia.RuntimeEffect +import org.jetbrains.skia.RuntimeShaderBuilder + +@Language("GLSL") // Technically, SkSL +private const val FOIL_SHADER_CODE = """ +const float SCALE = 1.8; // Effect scale (> 1 means smaller rainbow) +const float SATURATION = 0.9; // Color saturation (0.0 = grayscale, 1.0 = full color) +const float LIGHTNESS = 0.65; // Color lightness (0.0 = black, 1.0 = white) + +uniform shader content; // Input texture (the application canvas) +uniform vec2 resolution; // Size of the canvas +uniform vec2 offset; // Additional offset of the effect +uniform float intensity; // 0.0 = no effect, 1.0 = full effect + +// From https://www.ryanjuckett.com/photoshop-blend-modes-in-hlsl/ +vec3 BlendMode_Screen(vec3 base, vec3 blend) { + return base + blend - base * blend; +} + +vec4 rainbowEffect(vec2 uv, vec2 coord, vec2 offset) { + vec4 srcColor = content.eval(coord); + if (srcColor.a == 0.0) return srcColor; + + float hue = uv.x / (1.75 + abs(offset.x)) + offset.x / 3.0; + float lightness = LIGHTNESS + 0.25 * (0.5 + offset.y * (0.5 - uv.y)); + hue = fract(hue); + + float c = (1.0 - abs(2.0 * lightness - 1.0)) * SATURATION; + float x = c * (1.0 - abs(mod(hue / (1.0 / 6.0), 2.0) - 1.0)); + float m = LIGHTNESS - c / 2.0; + + vec3 rainbowPrime; + + if (hue < 1.0 / 6.0) { + rainbowPrime = vec3(c, x, 0.0); + } else if (hue < 1.0 / 3.0) { + rainbowPrime = vec3(x, c, 0.0); + } else if (hue < 0.5) { + rainbowPrime = vec3(0.0, c, x); + } else if (hue < 2.0 / 3.0) { + rainbowPrime = vec3(0.0, x, c); + } else if (hue < 5.0 / 6.0) { + rainbowPrime = vec3(x, 0.0, c); + } else { + rainbowPrime = vec3(c, 0.0, x); + } + + vec3 rainbow = BlendMode_Screen(srcColor.rgb, rainbowPrime + m); + return mix(srcColor, vec4(rainbow, srcColor.a), intensity); +} + +vec4 chromaticAberration(vec2 coord, vec2 offset) { + vec2 uv = coord / (resolution / SCALE); + vec4 srcColor = rainbowEffect(uv, coord, offset); + vec2 shift = offset * vec2(3.0, 5.0) / 1000.0; + vec4 leftColor = rainbowEffect(uv - shift, coord, offset); + vec4 rightColor = rainbowEffect(uv + shift, coord , offset); + + return vec4(rightColor.r, srcColor.g, leftColor.b, srcColor.a); +} + +vec4 main(float2 fragCoord) { + return chromaticAberration(fragCoord.xy, offset); +} +""" + +private val runtimeEffect = RuntimeEffect.makeForShader(FOIL_SHADER_CODE) +private val shaderBuilder = RuntimeShaderBuilder(runtimeEffect) + +internal fun Modifier.holoFoil(offset: Float, intensity: Float = 1f) = + graphicsLayer { + shaderBuilder.uniform("resolution", size.width, size.height) + shaderBuilder.uniform("offset", 0f, offset) + shaderBuilder.uniform("intensity", intensity * .65f) + + renderEffect = + ImageFilter.makeRuntimeShader( + runtimeShaderBuilder = shaderBuilder, + shaderNames = arrayOf("content"), + inputs = arrayOf(null), + ).asComposeRenderEffect() + + rotationX = offset * 4f * intensity + rotationY = offset * 10f * intensity + rotationZ = offset * -3f * intensity + scaleX = 1f - .1f * intensity + scaleY = 1f - .1f * intensity + } diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ImageComponent.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ImageComponent.kt new file mode 100644 index 000000000..d25133749 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ImageComponent.kt @@ -0,0 +1,135 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.openapi.application.EDT +import com.intellij.openapi.ui.GraphicsConfig +import com.intellij.ui.util.maximumHeight +import com.intellij.ui.util.maximumWidth +import com.intellij.util.ui.ImageUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.awt.Dimension +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.Image +import java.awt.event.ComponentEvent +import java.awt.event.ComponentListener +import java.awt.image.BufferedImage +import javax.swing.JComponent + +// TODO: figure out how to show a placeholder while the image is being loaded, +// while avoiding infinite loops of resized -> updateScaledImage() -> +// getPreferredSize() -> resized -> ... +internal class ImageComponent( + private val scope: CoroutineScope, + bufferedImage: BufferedImage? = null, +) : JComponent() { + + private var resizeJob: Job? = null + + var image: BufferedImage? = bufferedImage + set(value) { + if (field == value) return + field = value + updateScaledImage() + } + + private var scaledImage: Image? = null + + init { + addComponentListener(object : ComponentListener { + override fun componentResized(e: ComponentEvent?) { + updateScaledImage() + } + + override fun componentMoved(e: ComponentEvent?) { + // No-op + } + + override fun componentShown(e: ComponentEvent?) { + // No-op + } + + override fun componentHidden(e: ComponentEvent?) { + // No-op + } + }) + + registerUiInspectorInfoProvider { + mapOf( + "image" to image, + "imageSize" to image?.let { Dimension(ImageUtil.getUserWidth(it), ImageUtil.getUserHeight(it)) }, + ) + } + } + + @Suppress("InjectDispatcher") // It's ok in a silly sample + private fun updateScaledImage() { + resizeJob?.cancel("New image") + + val currentImage = image ?: return + + resizeJob = scope.launch(Dispatchers.Default) { + val imageWidth = currentImage.width + + val componentWidth = width + val ratioToFit = componentWidth.toDouble() / imageWidth + + val newImage = ImageUtil.scaleImage(currentImage, ratioToFit) + + launch(Dispatchers.EDT) { + scaledImage = newImage + revalidate() + } + } + } + + override fun getPreferredSize(): Dimension { + val currentImage = scaledImage + + return if (!isPreferredSizeSet && currentImage != null) { + val dimension = Dimension( + ImageUtil.getRealWidth(currentImage).coerceAtMost(maximumWidth), + ImageUtil.getRealHeight(currentImage).coerceAtMost(maximumHeight), + ) + dimension + } else { + super.getPreferredSize() + } + } + + override fun paintComponent(g: Graphics) { + val currentImage = scaledImage ?: return + + with(g as Graphics2D) { + val graphicsConfig = GraphicsConfig(this) + graphicsConfig.setupAAPainting() + + val imageWidth = ImageUtil.getUserWidth(currentImage) + val imageHeight = ImageUtil.getUserHeight(currentImage) + + val componentWidth = width + val componentHeight = height + + drawImage( + /* img = */ + currentImage, + /* x = */ + componentWidth / 2 - (imageWidth) / 2, + /* y = */ + componentHeight / 2 - (imageHeight) / 2, + /* observer = */ + null, + ) + + graphicsConfig.restore() + } + } + + override fun removeNotify() { + super.removeNotify() + resizeJob?.cancel("Detaching") + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleaseChannel.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleaseChannel.kt new file mode 100644 index 000000000..ac6fc7710 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleaseChannel.kt @@ -0,0 +1,68 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.ui.JBColor +import java.awt.Color + +@Suppress("UnregisteredNamedColor") // They exist at runtime +enum class ReleaseChannel( + val background: Color, + val foreground: Color, +) { + + Stable( + background = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Green10", 0xE6F7E9), + /* dark = */ + JBColor.namedColor("ColorPalette.Green3", 0x436946), + ), + foreground = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Green5", 0x369650), + /* dark = */ + JBColor.namedColor("ColorPalette.Green6", 0x5FAD65), + ), + ), + Beta( + background = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Yellow10", 0xFCEBA4), + /* dark = */ + JBColor.namedColor("ColorPalette.Yellow3", 0x826A41), + ), + foreground = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Yellow4", 0xFFAF0F), + /* dark = */ + JBColor.namedColor("ColorPalette.Yellow6", 0xD6AE58), + ), + ), + Canary( + background = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Orange8", 0xEC8F4C), + /* dark = */ + JBColor.namedColor("ColorPalette.Orange3", 0x825845), + ), + foreground = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Orange5", 0xEC8F4C), + /* dark = */ + JBColor.namedColor("ColorPalette.Orange6", 0xE08855), + ), + ), + Other( + background = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Grey12", 0xEBECF0), + /* dark = */ + JBColor.namedColor("ColorPalette.Grey5", 0x4E5157), + ), + foreground = JBColor( + /* regular = */ + JBColor.namedColor("ColorPalette.Grey6", 0x6C707E), + /* dark = */ + JBColor.namedColor("ColorPalette.Grey10", 0xB4B8BF), + ), + ), +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSampleCompose.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSampleCompose.kt new file mode 100644 index 000000000..fe755282f --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSampleCompose.kt @@ -0,0 +1,595 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.onClick +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Transparent +import androidx.compose.ui.graphics.Color.Companion.Unspecified +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.ResourceLoader +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.intellij.ide.ui.laf.darcula.DarculaUIUtil +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.ui.NewUI +import com.intellij.ui.RelativeFont +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.toJavaLocalDate +import org.jetbrains.jewel.HorizontalSplitLayout +import org.jetbrains.jewel.Icon +import org.jetbrains.jewel.LocalResourceLoader +import org.jetbrains.jewel.PopupMenu +import org.jetbrains.jewel.SvgLoader +import org.jetbrains.jewel.Text +import org.jetbrains.jewel.TextField +import org.jetbrains.jewel.VerticalScrollbar +import org.jetbrains.jewel.bridge.SwingBridgeService +import org.jetbrains.jewel.bridge.SwingBridgeTheme +import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified +import org.jetbrains.jewel.bridge.retrieveStatelessIcon +import org.jetbrains.jewel.bridge.retrieveTextStyle +import org.jetbrains.jewel.bridge.toComposeColor +import org.jetbrains.jewel.bridge.toFontFamily +import org.jetbrains.jewel.foundation.lazy.SelectableLazyColumn +import org.jetbrains.jewel.foundation.lazy.SelectionMode +import org.jetbrains.jewel.foundation.lazy.items +import org.jetbrains.jewel.foundation.lazy.rememberSelectableLazyListState +import org.jetbrains.jewel.foundation.onHover +import org.jetbrains.jewel.foundation.utils.thenIf +import org.jetbrains.jewel.intui.standalone.IntUiTheme +import org.jetbrains.jewel.items +import org.jetbrains.skiko.DependsOnJBR +import java.awt.Font +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import kotlin.time.Duration.Companion.seconds + +@OptIn(DependsOnJBR::class) +@Composable +fun ReleasesSampleCompose(project: Project) { + SwingBridgeTheme { + val svgLoader = service().svgLoader + + var selectedItem: ContentItem? by remember { mutableStateOf(null) } + HorizontalSplitLayout( + first = { modifier -> + LeftColumn( + project = project, + svgLoader = svgLoader, + modifier = modifier.fillMaxSize(), + onSelectedItemChange = { selectedItem = it }, + ) + }, + second = { modifier -> + RightColumn( + selectedItem = selectedItem, + modifier = modifier.fillMaxSize(), + ) + }, + Modifier.fillMaxSize(), + initialDividerPosition = 400.dp, + minRatio = .15f, + maxRatio = .7f, + ) + } +} + +@Composable +fun LeftColumn( + project: Project, + svgLoader: SvgLoader, + modifier: Modifier = Modifier, + onSelectedItemChange: (ContentItem?) -> Unit, +) { + val service = remember(project) { project.service() } + val currentContentSource by service.content.collectAsState() + + Column(modifier) { + Row( + modifier = Modifier.fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(4.dp, 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Filter elements:") + + Spacer(Modifier.width(8.dp)) + + val resourceLoader = LocalResourceLoader.current + SearchBar(service, svgLoader, resourceLoader, Modifier.weight(1f)) + + Spacer(Modifier.width(4.dp)) + + OverflowMenu(currentContentSource, svgLoader, resourceLoader) { + service.setContentSource(it) + } + } + + val listState = rememberSelectableLazyListState() + Box(modifier) { + SelectableLazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + selectionMode = SelectionMode.Single, + onSelectedIndexesChanged = { + val selectedItem = if (it.isNotEmpty()) { + currentContentSource.items[it.first()] + } else { + null + } + + onSelectedItemChange(selectedItem) + }, + ) { + items( + items = currentContentSource.items, + key = { it.key }, + contentType = { + when (it) { + is ContentItem.AndroidRelease -> ItemType.AndroidRelease + is ContentItem.AndroidStudio -> ItemType.AndroidStudio + } + }, + ) { + ContentItemRow(it, isSelected, isActive) { newFilter -> + service.filterContent(newFilter) + } + } + } + + VerticalScrollbar( + adapter = rememberScrollbarAdapter(listState.lazyListState), + modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd), + ) + } + } +} + +@Composable +private fun ContentItemRow( + item: ContentItem, + isSelected: Boolean, + isActive: Boolean, + onTagClick: (String) -> Unit, +) { + val color = when { + isSelected && isActive -> retrieveColorOrUnspecified("List.selectionBackground") + isSelected && !isActive -> retrieveColorOrUnspecified("List.selectionInactiveBackground") + else -> Transparent + } + Row( + modifier = Modifier.height(IntUiTheme.globalMetrics.rowHeight) + .background(color) + .padding(start = 4.dp, end = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = item.displayText, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + val pointerModifier = Modifier.pointerHoverIcon(PointerIcon.Hand) + when (item) { + is ContentItem.AndroidRelease -> { + ItemTag( + text = "API level ${item.apiLevel}", + backgroundColor = ReleaseChannel.Other.background.toComposeColor(), + foregroundColor = ReleaseChannel.Other.foreground.toComposeColor(), + modifier = pointerModifier.onClick { onTagClick(item.apiLevel.toString()) }, + ) + } + + is ContentItem.AndroidStudio -> { + val channel = item.channel + ItemTag( + text = channel.name.lowercase(), + backgroundColor = channel.background.toComposeColor(), + foregroundColor = channel.foreground.toComposeColor(), + modifier = pointerModifier.onClick { onTagClick(item.channel.name) }, + ) + } + } + } +} + +@Composable +private fun ItemTag( + text: String, + backgroundColor: Color, + foregroundColor: Color, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(4.dp), + padding: PaddingValues = PaddingValues(horizontal = 4.dp, vertical = 2.dp), +) { + Text( + text = text, + fontSize = JBFont.medium().size2D.sp, + color = foregroundColor, + modifier = modifier + .background(backgroundColor, shape) + .padding(padding), + ) +} + +private enum class ItemType { + AndroidRelease, AndroidStudio +} + +@Composable +private fun SearchBar( + service: ReleasesSampleService, + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, + modifier: Modifier = Modifier, +) { + val filterText by service.filter.collectAsState() + + val searchIconProvider = retrieveStatelessIcon("actions/search.svg", svgLoader, IntUiTheme.iconData) + val searchIcon by searchIconProvider.getPainter(resourceLoader) + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + TextField( + value = filterText, + onValueChange = { service.filterContent(it) }, + modifier = modifier.focusRequester(focusRequester), + leadingIcon = { + Icon(searchIcon, null, Modifier.padding(end = 8.dp)) + }, + trailingIcon = { + if (filterText.isNotBlank()) { + CloseIconButton(svgLoader, resourceLoader, service) + } + }, + ) +} + +@Composable +private fun CloseIconButton( + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, + service: ReleasesSampleService, +) { + val interactionSource = remember { MutableInteractionSource() } + var hovered by remember { mutableStateOf(false) } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { + when (it) { + is HoverInteraction.Enter -> hovered = true + is HoverInteraction.Exit -> hovered = false + } + } + } + + val closeIconProvider = retrieveStatelessIcon("actions/close.svg", svgLoader, IntUiTheme.iconData) + val closeIcon by closeIconProvider.getPainter(resourceLoader) + + val hoveredCloseIconProvider = + retrieveStatelessIcon("actions/closeHovered.svg", svgLoader, IntUiTheme.iconData) + val hoveredCloseIcon by hoveredCloseIconProvider.getPainter(resourceLoader) + + Icon( + painter = if (hovered) hoveredCloseIcon else closeIcon, + contentDescription = "Clear", + modifier = Modifier + .pointerHoverIcon(PointerIcon.Default) + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Button, + ) { service.resetFilter() }, + ) +} + +@Composable +private fun OverflowMenu( + currentContentSource: ContentSource<*>, + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, + onContentSourceChange: (ContentSource<*>) -> Unit, +) { + val iconProvider = + retrieveStatelessIcon("actions/more.svg", svgLoader, IntUiTheme.iconData) + val icon by iconProvider.getPainter(resourceLoader) + + val interactionSource = remember { MutableInteractionSource() } + var hovered by remember { mutableStateOf(false) } + var pressed by remember { mutableStateOf(false) } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { + when (it) { + is HoverInteraction.Enter -> hovered = true + is HoverInteraction.Exit -> hovered = false + is PressInteraction.Press -> pressed = true + is PressInteraction.Release, is PressInteraction.Cancel -> pressed = false + } + } + } + + val backgroundColor = remember(hovered, pressed) { + when { + pressed -> JBUI.CurrentTheme.ActionButton.pressedBackground().toComposeColor() + hovered -> JBUI.CurrentTheme.ActionButton.hoverBackground().toComposeColor() + else -> Unspecified + } + } + + var menuVisible by remember { mutableStateOf(false) } + + // TODO use IconButton when it exists + Icon( + painter = icon, + contentDescription = "Select data source", + modifier = Modifier + .fillMaxHeight() + .clickable(interactionSource, null) { menuVisible = !menuVisible } + .background(backgroundColor, RoundedCornerShape((DarculaUIUtil.BUTTON_ARC.unscaled / 2).dp)), + ) + + val contentSources = remember { + listOf(AndroidStudioReleases, AndroidReleases) + } + + if (menuVisible) { + val checkedIconProvider = + retrieveStatelessIcon("actions/checked.svg", svgLoader, IntUiTheme.iconData) + val checkedIcon by checkedIconProvider.getPainter(resourceLoader) + + PopupMenu( + onDismissRequest = { + menuVisible = false + true + }, + horizontalAlignment = Alignment.End, + content = { + items( + contentSources, + isSelected = { + // TODO fix this check once the "selected" check works correctly; + // the selected flag should be set on mouse hover/keyboard navigation + it.isSameAs(currentContentSource) + }, + onItemClick = onContentSourceChange, + ) { + Row( + modifier = Modifier.height(IntUiTheme.globalMetrics.rowHeight), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + ) { + if (it.isSameAs(currentContentSource)) { + Icon(checkedIcon, null) + } else { + val iconWidth = (checkedIcon.intrinsicSize.width / LocalDensity.current.density).dp + Spacer(Modifier.width(iconWidth)) + } + + Text(it.displayName) + } + } + }, + resourceLoader = resourceLoader, + ) + } +} + +@DependsOnJBR +@Composable +fun RightColumn( + selectedItem: ContentItem?, + modifier: Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + if (selectedItem == null) { + Text("Nothing to see here", color = JBUI.CurrentTheme.Label.disabledForeground().toComposeColor()) + } else { + val scrollState = rememberScrollState() + VerticalScrollbarContainer(scrollState, modifier = modifier) { + Column( + modifier = Modifier.verticalScroll(scrollState), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + val imagePath = selectedItem.imagePath + if (imagePath != null) { + ReleaseImage(imagePath) + } + + ItemDetailsText(selectedItem) + } + } + } + } +} + +@Composable +private fun ReleaseImage(imagePath: String) { + val painter = painterResource(imagePath, LocalResourceLoader.current) + val transition = rememberInfiniteTransition("HoloFoil") + val offset by transition.animateFloat( + initialValue = -1f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + tween(durationMillis = 2.seconds.inWholeMilliseconds.toInt(), easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + "holoFoil offset", + ) + var isHovered by remember { mutableStateOf(false) } + var applyModifier by remember { mutableStateOf(false) } + val intensity by animateFloatAsState(if (isHovered) 1f else 0f, animationSpec = tween(300)) + + val scope = rememberCoroutineScope() + + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.fillMaxWidth() + .sizeIn(minHeight = 150.dp, maxHeight = 250.dp) + .onHover { newIsHovered -> + scope.launch { + isHovered = newIsHovered + if (!newIsHovered) delay(300) + applyModifier = newIsHovered + } + } + .thenIf(applyModifier) { holoFoil(offset, intensity) }, + contentScale = ContentScale.Fit, + ) +} + +@Composable +private fun VerticalScrollbarContainer( + scrollState: ScrollState, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Box(modifier) { + content() + + VerticalScrollbar( + adapter = rememberScrollbarAdapter(scrollState), + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), + ) + } +} + +@DependsOnJBR +@Composable +private fun ItemDetailsText(selectedItem: ContentItem) { + Column( + Modifier.padding(horizontal = 20.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + selectedItem.displayText, + style = runBlocking { + retrieveTextStyle( + key = "Label.font", + bold = true, + size = JBFont.h1().size2D.sp, + ) + }, + ) + + val formatter = + remember(Locale.current) { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } + val releaseDate = selectedItem.releaseDate + if (releaseDate != null) { + Text( + text = "Released on ${formatter.format(releaseDate.toJavaLocalDate())}", + fontSize = getCommentFontSize(), + color = JBUI.CurrentTheme.Label.disabledForeground().toComposeColor(), + ) + } + + Spacer(Modifier.height(20.dp)) + + when (selectedItem) { + is ContentItem.AndroidRelease -> AndroidReleaseDetails(selectedItem) + is ContentItem.AndroidStudio -> AndroidStudioReleaseDetails(selectedItem) + } + } +} + +@Composable +private fun AndroidReleaseDetails(item: ContentItem.AndroidRelease) { + TextWithLabel("Codename:", item.codename ?: "N/A") + TextWithLabel("Version:", item.versionName) + TextWithLabel("API level:", item.apiLevel.toString()) +} + +@Composable +private fun AndroidStudioReleaseDetails(item: ContentItem.AndroidStudio) { + TextWithLabel("Channel:", item.channel.name) + TextWithLabel("Version:", item.versionName) + TextWithLabel("IntelliJ Platform version:", item.platformVersion) + TextWithLabel("IntelliJ Platform build:", item.platformBuild) + TextWithLabel("Full build number:", item.build) +} + +@OptIn(DependsOnJBR::class) +@Composable +private fun TextWithLabel(labelText: String, valueText: String) { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Text(labelText) + val fontFamily = JBFont.label().asBold().toFontFamily() + Text(valueText, fontFamily = fontFamily, fontWeight = FontWeight.W600) + } +} + +// Logic from com.intellij.openapi.ui.panel.ComponentPanelBuilder#getCommentFont +private fun getCommentFontSize(font: Font = JBFont.label()): TextUnit { + val commentFont = if (NewUI.isEnabled()) { + JBFont.medium() + } else { + RelativeFont.NORMAL.fromResource("ContextHelp.fontSizeOffset", -2).derive(font) + } + return commentFont.size2D.sp +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSamplePanel.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSamplePanel.kt new file mode 100644 index 000000000..c026005e2 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSamplePanel.kt @@ -0,0 +1,174 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.openapi.actionSystem.ActionToolbar +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ex.CheckboxAction +import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.openapi.actionSystem.impl.MoreActionGroup +import com.intellij.openapi.project.DumbAware +import com.intellij.ui.OnePixelSplitter +import com.intellij.ui.SearchTextField +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.scale.JBUIScale +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import kotlinx.coroutines.CoroutineScope +import javax.swing.BoxLayout +import javax.swing.DefaultListModel +import javax.swing.JPanel +import javax.swing.ListSelectionModel +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +class ReleasesSamplePanel(scope: CoroutineScope) : BorderLayoutPanel() { + + private val sidePanel = DetailsPanel(scope) + + private var currentContentSource: ContentSource<*> = AndroidStudioReleases + + private val filterTextField = SearchTextField(false).apply { + addDocumentListener(object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { + filterContent(text) + } + + override fun removeUpdate(e: DocumentEvent) { + filterContent(text) + } + + override fun changedUpdate(e: DocumentEvent) { + filterContent(text) + } + }) + } + + private val actions: List = listOf( + object : CheckboxAction(AndroidStudioReleases.displayName), DumbAware { + + override fun isSelected(e: AnActionEvent): Boolean = + currentContentSource == AndroidStudioReleases + + override fun setSelected(e: AnActionEvent, state: Boolean) { + setContentSource(AndroidStudioReleases) + } + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + }, + object : CheckboxAction(AndroidReleases.displayName), DumbAware { + + override fun isSelected(e: AnActionEvent): Boolean = + currentContentSource == AndroidReleases + + override fun setSelected(e: AnActionEvent, state: Boolean) { + setContentSource(AndroidReleases) + } + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + }, + ) + + private val overflowAction = MoreActionGroup() + .apply { addAll(actions) } + + private val overflowActionButton: ActionButton = + ActionButton( + overflowAction, + overflowAction.templatePresentation.clone(), + "JewelSwingDemoTopBar", + ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE, + ) + + private val topBar = BorderLayoutPanel().apply { + addToLeft(JBLabel("Filter elements: ")) + addToCenter(filterTextField) + addToRight(overflowActionButton) + border = JBUI.Borders.empty(4) + } + + private var lastSelected: ContentItem? = null + private val contentList = JBList().apply { + selectionMode = ListSelectionModel.SINGLE_SELECTION + + addListSelectionListener { + if (selectedValue != lastSelected) { + lastSelected = selectedValue + onListSelectionChanged() + } + } + } + + private val mainPanel = BorderLayoutPanel().apply { + addToTop(topBar) + + val scrollPane = JBScrollPane(contentList).apply { + setBorder(JBUI.Borders.empty()) + setViewportBorder(JBUI.Borders.empty()) + horizontalScrollBarPolicy = JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER + } + + addToCenter(scrollPane) + } + + init { + val splitter = OnePixelSplitter(false, .7f, .25f, .75f) + splitter.firstComponent = mainPanel + splitter.secondComponent = sidePanel + splitter.foreground + addToCenter(splitter) + + contentList.installCellRenderer { + BorderLayoutPanel(JBUIScale.scale(4), 0).apply { + border = JBUI.Borders.empty(0, 4) + + addToCenter(JBLabel(it.displayText)) + + if (it is ContentItem.AndroidStudio) { + addToRight( + JPanel().apply { + layout = BoxLayout(this, BoxLayout.LINE_AXIS) + isOpaque = false + add(ChannelIndication(it.channel)) + }, + ) + } else if (it is ContentItem.AndroidRelease) { + addToRight( + JPanel().apply { + layout = BoxLayout(this, BoxLayout.LINE_AXIS) + isOpaque = false + add(ApiLevelIndication(it.apiLevel)) + }, + ) + } + } + } + + setContentSource(AndroidStudioReleases) + } + + private fun setContentSource(contentSource: ContentSource<*>) { + currentContentSource = contentSource + + contentList.model = JBList.createDefaultListModel(contentSource.items) + } + + private fun filterContent(text: String) { + val model = contentList.model as DefaultListModel + + val normalizedFilter = text.trim() + + model.clear() + model.addAll(currentContentSource.items.filter { it.matches(normalizedFilter) }) + } + + private fun onListSelectionChanged() { + val selection = contentList.selectedValue + sidePanel.display(selection) + + revalidate() + repaint() + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSampleService.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSampleService.kt new file mode 100644 index 000000000..c68d27060 --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/ReleasesSampleService.kt @@ -0,0 +1,66 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.util.concurrency.AppExecutorUtil +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@Service(Service.Level.PROJECT) +internal class ReleasesSampleService : CoroutineScope, Disposable { + + private val dispatcher = AppExecutorUtil.getAppExecutorService().asCoroutineDispatcher() + + override val coroutineContext = SupervisorJob() + CoroutineName("ReleasesSampleService") + dispatcher + + private val originalContentSource = MutableStateFlow>(AndroidStudioReleases) + + private val _filter = MutableStateFlow("") + val filter = _filter.asStateFlow() + + private val filteredContent = MutableStateFlow(originalContentSource.value) + val content = filteredContent.asStateFlow() + + init { + combine(originalContentSource, filter) { source, filter -> + val normalizedFilter = filter.trim() + if (normalizedFilter.isBlank()) return@combine source + + val filteredContentItems = source.items + .filter { it.matches(normalizedFilter) } + + FilteredContentSource(filteredContentItems, source) + } + .onEach { filteredContent.emit(it) } + .launchIn(this) + } + + fun setContentSource(contentSource: ContentSource<*>) { + if (contentSource != originalContentSource.value) { + originalContentSource.tryEmit(contentSource) + resetFilter() + } + } + + fun resetFilter() { + filterContent("") + } + + fun filterContent(filter: String) { + _filter.tryEmit(filter) + } + + override fun dispose() { + cancel("Disposing ${this::class.simpleName}...") + coroutineContext.cancel(CancellationException("Shutting down project...")) + } +} diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/UiInspectorUtil.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/UiInspectorUtil.kt new file mode 100644 index 000000000..5374ec82d --- /dev/null +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/releasessample/UiInspectorUtil.kt @@ -0,0 +1,11 @@ +package org.jetbrains.jewel.samples.ideplugin.releasessample + +import com.intellij.internal.inspector.PropertyBean +import com.intellij.internal.inspector.UiInspectorUtil +import javax.swing.JComponent + +fun JComponent.registerUiInspectorInfoProvider(provider: () -> Map) { + UiInspectorUtil.registerProvider(this) { + provider().map { (key, value) -> PropertyBean(key, value) } + } +} diff --git a/samples/ide-plugin/src/main/resources/JewelIntUiIconMappings.json b/samples/ide-plugin/src/main/resources/JewelIntUiIconMappings.json new file mode 100644 index 000000000..e69ad5c18 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/JewelIntUiIconMappings.json @@ -0,0 +1,7 @@ +{ + "intui": { + "icons": { + "jewel-tool-window.svg": "icons/jewel-tool-window.svg" + } + } +} diff --git a/samples/ide-plugin/src/main/resources/META-INF/plugin.xml b/samples/ide-plugin/src/main/resources/META-INF/plugin.xml index e23ed6944..55ec8440c 100644 --- a/samples/ide-plugin/src/main/resources/META-INF/plugin.xml +++ b/samples/ide-plugin/src/main/resources/META-INF/plugin.xml @@ -11,9 +11,14 @@ See the Jewel repository for mo com.intellij.modules.platform + messages.JewelBundle + + icon="JewelIcons.ToolWindowIcon" + factoryClass="org.jetbrains.jewel.samples.ideplugin.JewelDemoToolWindowFactory"/> + + diff --git a/samples/ide-plugin/src/main/resources/android-releases/10.svg b/samples/ide-plugin/src/main/resources/android-releases/10.svg new file mode 100644 index 000000000..af820c825 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/11.svg b/samples/ide-plugin/src/main/resources/android-releases/11.svg new file mode 100644 index 000000000..dfbe24b1a --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/11.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/12.svg b/samples/ide-plugin/src/main/resources/android-releases/12.svg new file mode 100755 index 000000000..e0e2bdc53 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/12.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/13.svg b/samples/ide-plugin/src/main/resources/android-releases/13.svg new file mode 100755 index 000000000..e2174aae6 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/13.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/14.svg b/samples/ide-plugin/src/main/resources/android-releases/14.svg new file mode 100644 index 000000000..790e77cfc --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/14.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/cupcake.png b/samples/ide-plugin/src/main/resources/android-releases/cupcake.png new file mode 100644 index 000000000..7dc9ee84a Binary files /dev/null and b/samples/ide-plugin/src/main/resources/android-releases/cupcake.png differ diff --git a/samples/ide-plugin/src/main/resources/android-releases/donut.png b/samples/ide-plugin/src/main/resources/android-releases/donut.png new file mode 100644 index 000000000..16f1ee646 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/android-releases/donut.png differ diff --git a/samples/ide-plugin/src/main/resources/android-releases/eclair.png b/samples/ide-plugin/src/main/resources/android-releases/eclair.png new file mode 100644 index 000000000..d92e98060 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/android-releases/eclair.png differ diff --git a/samples/ide-plugin/src/main/resources/android-releases/froyo.png b/samples/ide-plugin/src/main/resources/android-releases/froyo.png new file mode 100644 index 000000000..50df2f6c2 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/android-releases/froyo.png differ diff --git a/samples/ide-plugin/src/main/resources/android-releases/gingerbread.png b/samples/ide-plugin/src/main/resources/android-releases/gingerbread.png new file mode 100644 index 000000000..1f661b763 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/android-releases/gingerbread.png differ diff --git a/samples/ide-plugin/src/main/resources/android-releases/honeycomb.svg b/samples/ide-plugin/src/main/resources/android-releases/honeycomb.svg new file mode 100644 index 000000000..d8faf2fd8 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/honeycomb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/ice-cream-sandwich.svg b/samples/ide-plugin/src/main/resources/android-releases/ice-cream-sandwich.svg new file mode 100644 index 000000000..e9539435a --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/ice-cream-sandwich.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/jelly-bean.svg b/samples/ide-plugin/src/main/resources/android-releases/jelly-bean.svg new file mode 100644 index 000000000..c47aadc16 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/jelly-bean.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/samples/ide-plugin/src/main/resources/android-releases/kitkat.svg b/samples/ide-plugin/src/main/resources/android-releases/kitkat.svg new file mode 100644 index 000000000..7e02f482e --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/kitkat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/lollipop.svg b/samples/ide-plugin/src/main/resources/android-releases/lollipop.svg new file mode 100644 index 000000000..b7bdce32b --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/lollipop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/marshmallow.svg b/samples/ide-plugin/src/main/resources/android-releases/marshmallow.svg new file mode 100644 index 000000000..df4bfeedc --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/marshmallow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/nougat.svg b/samples/ide-plugin/src/main/resources/android-releases/nougat.svg new file mode 100644 index 000000000..b3faa911f --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/nougat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/oreo.svg b/samples/ide-plugin/src/main/resources/android-releases/oreo.svg new file mode 100644 index 000000000..731ee79a4 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/oreo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ide-plugin/src/main/resources/android-releases/pie.svg b/samples/ide-plugin/src/main/resources/android-releases/pie.svg new file mode 100644 index 000000000..157e356c3 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/android-releases/pie.svg @@ -0,0 +1,7 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/icons/jewel-tool-window.svg b/samples/ide-plugin/src/main/resources/icons/jewel-tool-window.svg new file mode 100644 index 000000000..1ac58ace9 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/icons/jewel-tool-window.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/icons/jewel-tool-window_dark.svg b/samples/ide-plugin/src/main/resources/icons/jewel-tool-window_dark.svg new file mode 100644 index 000000000..23c4e03c5 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/icons/jewel-tool-window_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window.svg b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window.svg new file mode 100644 index 000000000..4782437da --- /dev/null +++ b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window@20x20.svg b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window@20x20.svg new file mode 100644 index 000000000..a2fb03f58 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window@20x20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window@20x20_dark.svg b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window@20x20_dark.svg new file mode 100644 index 000000000..864a1e5fa --- /dev/null +++ b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window@20x20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window_dark.svg b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window_dark.svg new file mode 100644 index 000000000..ef0e5b3f8 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/intui/icons/jewel-tool-window_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/ide-plugin/src/main/resources/messages/JewelBundle.properties b/samples/ide-plugin/src/main/resources/messages/JewelBundle.properties new file mode 100644 index 000000000..efa86d347 --- /dev/null +++ b/samples/ide-plugin/src/main/resources/messages/JewelBundle.properties @@ -0,0 +1 @@ +toolwindow.stripe.JewelDemo=Jewel Demo diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/ArcticFox-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/ArcticFox-stable.png new file mode 100755 index 000000000..5e927611a Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/ArcticFox-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Bumblebee-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Bumblebee-canary.png new file mode 100755 index 000000000..c0cb7b17b Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Bumblebee-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Bumblebee-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Bumblebee-stable.png new file mode 100755 index 000000000..70122a27d Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Bumblebee-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Chipmunk-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Chipmunk-canary.png new file mode 100755 index 000000000..1856d4f62 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Chipmunk-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Chipmunk-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Chipmunk-stable.png new file mode 100755 index 000000000..7a1969314 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Chipmunk-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Dolphin-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Dolphin-canary.png new file mode 100755 index 000000000..7db3acfd9 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Dolphin-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Dolphin-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Dolphin-stable.png new file mode 100755 index 000000000..ec416e920 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Dolphin-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/ElectricEel-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/ElectricEel-canary.png new file mode 100755 index 000000000..0247a1b3c Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/ElectricEel-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/ElectricEel-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/ElectricEel-stable.png new file mode 100755 index 000000000..5050ca141 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/ElectricEel-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Flamingo-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Flamingo-canary.png new file mode 100755 index 000000000..2d25baa17 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Flamingo-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Flamingo-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Flamingo-stable.png new file mode 100755 index 000000000..d3ece2ef0 Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Flamingo-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Giraffe-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Giraffe-canary.png new file mode 100755 index 000000000..632918b9d Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Giraffe-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Giraffe-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Giraffe-stable.png new file mode 100755 index 000000000..de891dcab Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Giraffe-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Hedgehog-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Hedgehog-canary.png new file mode 100755 index 000000000..dbb6a83bf Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Hedgehog-canary.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Hedgehog-stable.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Hedgehog-stable.png new file mode 100755 index 000000000..af84e1e8c Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Hedgehog-stable.png differ diff --git a/samples/ide-plugin/src/main/resources/studio-splash-screens/Iguana-canary.png b/samples/ide-plugin/src/main/resources/studio-splash-screens/Iguana-canary.png new file mode 100755 index 000000000..972ea3ced Binary files /dev/null and b/samples/ide-plugin/src/main/resources/studio-splash-screens/Iguana-canary.png differ diff --git a/samples/standalone/build.gradle.kts b/samples/standalone/build.gradle.kts index 320666bc5..f7651827e 100644 --- a/samples/standalone/build.gradle.kts +++ b/samples/standalone/build.gradle.kts @@ -9,6 +9,9 @@ plugins { dependencies { implementation(projects.intUi.intUiStandalone) + implementation(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + } } compose.desktop { diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt index 60404f27f..0c47d64a9 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication import org.jetbrains.jewel.CheckboxRow import org.jetbrains.jewel.Divider +import org.jetbrains.jewel.JewelSvgLoader import org.jetbrains.jewel.LocalResourceLoader import org.jetbrains.jewel.Orientation import org.jetbrains.jewel.VerticalScrollbar @@ -36,11 +37,13 @@ import org.jetbrains.jewel.intui.standalone.IntUiTheme import org.jetbrains.jewel.intui.standalone.ToolWindow import org.jetbrains.jewel.intui.standalone.ToolWindowButtonData import org.jetbrains.jewel.intui.standalone.styling.IntUiToolWindowButtonStyle +import org.jetbrains.jewel.intui.standalone.rememberSvgLoader import org.jetbrains.jewel.samples.standalone.components.Borders import org.jetbrains.jewel.samples.standalone.components.Buttons import org.jetbrains.jewel.samples.standalone.components.Checkboxes import org.jetbrains.jewel.samples.standalone.components.ChipsAndTree import org.jetbrains.jewel.samples.standalone.components.Dropdowns +import org.jetbrains.jewel.samples.standalone.components.Icons import org.jetbrains.jewel.samples.standalone.components.Links import org.jetbrains.jewel.samples.standalone.components.ProgressBar import org.jetbrains.jewel.samples.standalone.components.RadioButtons @@ -48,6 +51,7 @@ import org.jetbrains.jewel.samples.standalone.components.Tabs import org.jetbrains.jewel.samples.standalone.components.TextAreas import org.jetbrains.jewel.samples.standalone.components.TextFields import org.jetbrains.jewel.samples.standalone.components.ToolWindowStrip +import org.jetbrains.jewel.samples.standalone.components.Tooltips import java.io.InputStream fun main() { @@ -62,6 +66,7 @@ fun main() { IntUiTheme(theme, swingCompat) { val resourceLoader = LocalResourceLoader.current + val svgLoader by rememberSvgLoader() val windowBackground = if (isDark) { IntUiTheme.colorPalette.grey(1) @@ -127,9 +132,9 @@ fun main() { CheckboxRow("Swing compat", swingCompat, resourceLoader, { swingCompat = it }) } - Divider(Modifier.fillMaxWidth()) + Divider(Orientation.Horizontal, Modifier.fillMaxWidth()) - ComponentShowcase() + ComponentShowcase(svgLoader, resourceLoader) } } } @@ -137,9 +142,8 @@ fun main() { } @Composable -private fun ComponentShowcase() { +private fun ComponentShowcase(svgLoader: JewelSvgLoader, resourceLoader: ResourceLoader) { val verticalScrollState = rememberScrollState() - Box(Modifier.fillMaxSize()) { Column( Modifier.width(IntrinsicSize.Max) @@ -149,16 +153,18 @@ private fun ComponentShowcase() { horizontalAlignment = Alignment.Start, ) { Borders() - Buttons() + Buttons(svgLoader, resourceLoader) Dropdowns() Checkboxes() RadioButtons() Links() - TextFields() + Tooltips() + TextFields(svgLoader, resourceLoader) TextAreas() - ProgressBar() + ProgressBar(svgLoader) ChipsAndTree() - Tabs() + Tabs(svgLoader, resourceLoader) + Icons(svgLoader, resourceLoader) } VerticalScrollbar( diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Buttons.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Buttons.kt index 3096ed884..fc9213cd5 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Buttons.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Buttons.kt @@ -4,16 +4,22 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.ResourceLoader import androidx.compose.ui.unit.dp import org.jetbrains.jewel.DefaultButton import org.jetbrains.jewel.GroupHeader +import org.jetbrains.jewel.Icon +import org.jetbrains.jewel.IconButton +import org.jetbrains.jewel.JewelSvgLoader import org.jetbrains.jewel.OutlinedButton import org.jetbrains.jewel.Text +import org.jetbrains.jewel.styling.rememberStatelessPainterProvider @Composable -fun Buttons() { +fun Buttons(svgLoader: JewelSvgLoader, resourceLoader: ResourceLoader) { GroupHeader("Buttons") Row( modifier = Modifier.fillMaxWidth(), @@ -35,5 +41,14 @@ fun Buttons() { DefaultButton(onClick = {}, enabled = false) { Text("Default disabled") } + + IconButton(onClick = {}) { + val iconProvider = rememberStatelessPainterProvider("icons/close.svg", svgLoader) + val iconPainter by iconProvider.getPainter(resourceLoader) + Icon( + painter = iconPainter, + "icon", + ) + } } } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Dropdowns.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Dropdowns.kt index 98cbd5562..a097d2291 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Dropdowns.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Dropdowns.kt @@ -42,7 +42,7 @@ fun Dropdowns() { menuContent = { }, ) { - Text(selected) + Text("Disabled") } Dropdown( resourceLoader = resourceLoader, diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt new file mode 100644 index 000000000..71c494d84 --- /dev/null +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt @@ -0,0 +1,38 @@ +package org.jetbrains.jewel.samples.standalone.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.ResourceLoader +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.GroupHeader +import org.jetbrains.jewel.Icon +import org.jetbrains.jewel.SvgLoader +import org.jetbrains.jewel.styling.rememberStatelessPainterProvider + +@Composable +internal fun Icons(svgLoader: SvgLoader, resourceLoader: ResourceLoader) { + GroupHeader("Icons") + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + val jewelLogoProvider = rememberStatelessPainterProvider("icons/jewel-logo.svg", svgLoader) + val jewelLogo by jewelLogoProvider.getPainter(resourceLoader) + + Icon(jewelLogo, "Jewel Logo", Modifier.size(16.dp)) + Icon(jewelLogo, "Jewel Logo", Modifier.size(32.dp)) + Icon(jewelLogo, "Jewel Logo", Modifier.size(64.dp)) + Icon(jewelLogo, "Jewel Logo", Modifier.size(128.dp)) + Icon(jewelLogo, "Jewel Logo", ColorFilter.tint(Color.Magenta, BlendMode.Multiply), Modifier.size(128.dp)) + } +} diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/ProgressBar.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/ProgressBar.kt index ae8d6d778..89e944f0d 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/ProgressBar.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/ProgressBar.kt @@ -14,16 +14,20 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay +import org.jetbrains.jewel.CircularProgressIndicator +import org.jetbrains.jewel.CircularProgressIndicatorBig import org.jetbrains.jewel.GroupHeader import org.jetbrains.jewel.HorizontalProgressBar import org.jetbrains.jewel.IndeterminateHorizontalProgressBar +import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.Text @Composable -fun ProgressBar() { +fun ProgressBar(svgLoader: SvgLoader) { GroupHeader("Progress bars") val transition = rememberInfiniteTransition() val currentOffset by transition.animateFloat( @@ -50,21 +54,33 @@ fun ProgressBar() { } Column { Text("HorizontalProgressBar - linear progress") - Row(Modifier.width(600.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Row( + Modifier.width(600.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { HorizontalProgressBar(modifier = Modifier.width(500.dp), progress = currentOffset) Text("${(currentOffset * 100).toInt()} %") } } Column { Text("HorizontalProgressBar - non linear progress") - Row(Modifier.width(600.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Row( + Modifier.width(600.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { HorizontalProgressBar(modifier = Modifier.width(500.dp), progress = intermittentProgress) Text("${(intermittentProgress * 100).toInt()} %") } } Column { Text("HorizontalProgressBar - smoothed non linear progress") - Row(Modifier.width(600.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Row( + Modifier.width(600.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { val smoothedProgress by androidx.compose.animation.core.animateFloatAsState(intermittentProgress) HorizontalProgressBar(modifier = Modifier.width(500.dp), progress = smoothedProgress) Text("${(intermittentProgress * 100).toInt()} %") @@ -72,9 +88,31 @@ fun ProgressBar() { } Column { Text("IndeterminateHorizontalProgressBar") - Row(Modifier.width(600.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Row( + Modifier.width(600.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { IndeterminateHorizontalProgressBar(modifier = Modifier.width(500.dp)) Text("----") } } + Column { + Row( + Modifier.width(600.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("CircularProgress (16x16)") + CircularProgressIndicator(svgLoader) + } + Row( + Modifier.width(600.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("CircularProgressBig (32x32) - Big") + CircularProgressIndicatorBig(svgLoader) + } + } } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tabs.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tabs.kt index 1e4612b60..cf7015d06 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tabs.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tabs.kt @@ -2,14 +2,19 @@ package org.jetbrains.jewel.samples.standalone.components +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -17,28 +22,40 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.ResourceLoader +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import org.jetbrains.jewel.GroupHeader import org.jetbrains.jewel.Icon import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.NoIndication +import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.TabData import org.jetbrains.jewel.TabStrip import org.jetbrains.jewel.Text +import org.jetbrains.jewel.intui.standalone.IntUiTheme +import org.jetbrains.jewel.styling.rememberStatelessPainterProvider import kotlin.math.max @Composable -fun Tabs() { +fun Tabs( + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, +) { GroupHeader("Tabs") Text("Default tabs", Modifier.fillMaxWidth()) - DefaultTabShowcase() + DefaultTabShowcase(svgLoader, resourceLoader) Spacer(Modifier.height(16.dp)) Text("Editor tabs", Modifier.fillMaxWidth()) - EditorTabShowcase() + EditorTabShowcase(svgLoader, resourceLoader) } @Composable -private fun DefaultTabShowcase() { +private fun DefaultTabShowcase( + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, +) { var selectedTabIndex by remember { mutableStateOf(0) } var tabIds by remember { mutableStateOf((1..12).toList()) } @@ -62,7 +79,7 @@ private fun DefaultTabShowcase() { } } - TabStripWithAddButton(tabs = tabs) { + TabStripWithAddButton(tabs, svgLoader, resourceLoader) { val insertionIndex = (selectedTabIndex + 1).coerceIn(0..tabIds.size) val nextTabId = maxId + 1 @@ -73,7 +90,10 @@ private fun DefaultTabShowcase() { } @Composable -private fun EditorTabShowcase() { +private fun EditorTabShowcase( + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, +) { var selectedTabIndex by remember { mutableStateOf(0) } var tabIds by remember { mutableStateOf((1..12).toList()) } @@ -97,7 +117,7 @@ private fun EditorTabShowcase() { } } - TabStripWithAddButton(tabs = tabs) { + TabStripWithAddButton(tabs, svgLoader, resourceLoader) { val insertionIndex = (selectedTabIndex + 1).coerceIn(0..tabIds.size) val nextTabId = maxId + 1 @@ -110,6 +130,8 @@ private fun EditorTabShowcase() { @Composable private fun TabStripWithAddButton( tabs: List, + svgLoader: SvgLoader, + resourceLoader: ResourceLoader, onAddClick: () -> Unit, ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -117,11 +139,41 @@ private fun TabStripWithAddButton( Spacer(Modifier.width(8.dp)) - IconButton( - onClick = onAddClick, - modifier = Modifier.size(IntelliJTheme.defaultTabStyle.metrics.tabHeight), + var isHovered by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is HoverInteraction.Enter -> isHovered = true + is HoverInteraction.Exit -> isHovered = false + } + } + } + + // TODO create an IconButton instead of this hack + val backgroundColor = if (isHovered) { + IntUiTheme.defaultTabStyle.colors.backgroundHovered + } else { + IntUiTheme.defaultTabStyle.colors.background + } + + Box( + modifier = Modifier.size(IntelliJTheme.defaultTabStyle.metrics.tabHeight) + .clickable( + onClick = onAddClick, + onClickLabel = "Add a tab", + role = Role.Button, + interactionSource = interactionSource, + indication = NoIndication, + ) + .background(backgroundColor), + contentAlignment = Alignment.Center, ) { - Icon("icons/intui/add.svg", contentDescription = "Add a tab") + val addIconProvider = rememberStatelessPainterProvider("expui/general/add.svg", svgLoader) + val addIcon by addIconProvider.getPainter(resourceLoader) + + Icon(addIcon, contentDescription = "Add a tab") } } } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/TextFields.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/TextFields.kt index 5fcf04f32..83af58ef1 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/TextFields.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/TextFields.kt @@ -2,21 +2,28 @@ package org.jetbrains.jewel.samples.standalone.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.ResourceLoader import androidx.compose.ui.unit.dp import org.jetbrains.jewel.GroupHeader +import org.jetbrains.jewel.Icon import org.jetbrains.jewel.LabelledTextField +import org.jetbrains.jewel.LocalIconData import org.jetbrains.jewel.Outline +import org.jetbrains.jewel.SvgLoader import org.jetbrains.jewel.Text import org.jetbrains.jewel.TextField +import org.jetbrains.jewel.styling.rememberStatelessPainterProvider @Composable -fun TextFields() { +fun TextFields(svgLoader: SvgLoader, resourceLoader: ResourceLoader) { GroupHeader("TextFields") Row( horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -59,4 +66,18 @@ fun TextFields() { placeholder = { Text("Labelled TextField with hint") }, ) } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top, + ) { + var text by remember { mutableStateOf("With leading icon") } + TextField(text, { text = it }, enabled = true, leadingIcon = { + val iconData = LocalIconData.current + val searchIcon by rememberStatelessPainterProvider("icons/search.svg", svgLoader, iconData) + .getPainter( + resourceLoader, + ) + Icon(searchIcon, "SearchIcon", Modifier.size(16.dp)) + }) + } } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tooltips.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tooltips.kt new file mode 100644 index 000000000..0b3c38e06 --- /dev/null +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Tooltips.kt @@ -0,0 +1,21 @@ +package org.jetbrains.jewel.samples.standalone.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.GroupHeader +import org.jetbrains.jewel.Text +import org.jetbrains.jewel.Tooltip +import org.jetbrains.jewel.intui.standalone.IntUiTheme + +@Composable +fun Tooltips() { + GroupHeader("Tooltips") + Tooltip(tooltip = { + Text("This is a tooltip") + }) { + Text(modifier = Modifier.border(1.dp, IntUiTheme.globalColors.borders.normal).padding(4.dp), text = "Hover Me!") + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ef0906cc..0d8e601ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,7 @@ dependencyResolutionManagement { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://androidx.dev/storage/compose-compiler/repository/") maven("https://www.jetbrains.com/intellij-repository/releases") + maven("https://www.jetbrains.com/intellij-repository/snapshots") maven("https://cache-redirector.jetbrains.com/intellij-dependencies") mavenCentral() } @@ -27,6 +28,8 @@ dependencyResolutionManagement { include( ":core", ":ide-laf-bridge", + ":ide-laf-bridge:ide-laf-bridge-232", + ":ide-laf-bridge:ide-laf-bridge-233", ":samples:standalone", ":samples:ide-plugin", ":int-ui:int-ui-core",