From b15edc25c88f900fefca7e6ff06a4e196323c7be Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Thu, 5 Sep 2024 12:16:35 +0200 Subject: [PATCH 1/3] Remove one more usage of resource-based Icon APIs In this case, they were used in the Menus API. Not sure why find usages did not show these when I did the work for #576... --- .../standalone/view/component/Dropdowns.kt | 4 -- ui/api/ui.api | 9 +-- .../org/jetbrains/jewel/ui/component/Menu.kt | 55 +++++++++++-------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt index 81c73be1e..1f562c580 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt @@ -14,7 +14,6 @@ import org.jetbrains.jewel.ui.Outline import org.jetbrains.jewel.ui.component.Dropdown import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.separator -import org.jetbrains.jewel.ui.component.styling.DropdownStyle import org.jetbrains.jewel.ui.icons.AllIconsKeys @Composable @@ -89,7 +88,6 @@ fun Dropdowns() { } else { selectableItem( iconKey = dropdownIconsSample.random(), - iconClass = DropdownStyle::class.java, keybinding = if (Random.nextBoolean()) { null @@ -111,7 +109,6 @@ fun Dropdowns() { } else { selectableItem( iconKey = dropdownIconsSample.random(), - iconClass = DropdownStyle::class.java, keybinding = if (Random.nextBoolean()) { null @@ -134,7 +131,6 @@ fun Dropdowns() { } else { selectableItem( iconKey = dropdownIconsSample.random(), - iconClass = DropdownStyle::class.java, selected = false, onClick = {}, ) { diff --git a/ui/api/ui.api b/ui/api/ui.api index 8b063f572..8712f3052 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -497,6 +497,7 @@ public final class org/jetbrains/jewel/ui/component/MenuItemState$Companion { public final class org/jetbrains/jewel/ui/component/MenuKt { public static final fun MenuSeparator (Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/component/styling/MenuItemMetrics;Lorg/jetbrains/jewel/ui/component/styling/MenuItemColors;Landroidx/compose/runtime/Composer;II)V public static final fun MenuSubmenuItem (Landroidx/compose/ui/Modifier;ZZLjava/lang/String;Ljava/lang/Class;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun MenuSubmenuItem (Landroidx/compose/ui/Modifier;ZZLorg/jetbrains/jewel/ui/icon/IconKey;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V public static final fun PopupMenu (Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/component/styling/MenuStyle;Landroidx/compose/ui/window/PopupProperties;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun items (Lorg/jetbrains/jewel/ui/component/MenuScope;ILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)V public static final fun items (Lorg/jetbrains/jewel/ui/component/MenuScope;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)V @@ -521,13 +522,13 @@ public final class org/jetbrains/jewel/ui/component/MenuManagerKt { public abstract interface class org/jetbrains/jewel/ui/component/MenuScope { public abstract fun passiveItem (Lkotlin/jvm/functions/Function2;)V - public abstract fun selectableItem (ZLorg/jetbrains/jewel/ui/icon/IconKey;Ljava/lang/Class;Ljava/util/Set;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;)V - public abstract fun submenu (ZLjava/lang/String;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public abstract fun selectableItem (ZLorg/jetbrains/jewel/ui/icon/IconKey;Ljava/util/Set;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;)V + public abstract fun submenu (ZLorg/jetbrains/jewel/ui/icon/IconKey;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V } public final class org/jetbrains/jewel/ui/component/MenuScope$DefaultImpls { - public static synthetic fun selectableItem$default (Lorg/jetbrains/jewel/ui/component/MenuScope;ZLorg/jetbrains/jewel/ui/icon/IconKey;Ljava/lang/Class;Ljava/util/Set;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public static synthetic fun submenu$default (Lorg/jetbrains/jewel/ui/component/MenuScope;ZLjava/lang/String;Ljava/lang/Class;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun selectableItem$default (Lorg/jetbrains/jewel/ui/component/MenuScope;ZLorg/jetbrains/jewel/ui/icon/IconKey;Ljava/util/Set;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public static synthetic fun submenu$default (Lorg/jetbrains/jewel/ui/component/MenuScope;ZLorg/jetbrains/jewel/ui/icon/IconKey;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V } public final class org/jetbrains/jewel/ui/component/PlatformIconKt { diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt index 4317e9040..c76b78a70 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt @@ -82,6 +82,7 @@ import org.jetbrains.jewel.ui.component.styling.MenuItemColors import org.jetbrains.jewel.ui.component.styling.MenuItemMetrics import org.jetbrains.jewel.ui.component.styling.MenuStyle import org.jetbrains.jewel.ui.icon.IconKey +import org.jetbrains.jewel.ui.icon.PathIconKey import org.jetbrains.jewel.ui.painter.hints.Stateful import org.jetbrains.jewel.ui.theme.menuStyle import org.jetbrains.skiko.hostOs @@ -187,7 +188,6 @@ private fun ShowMenuItem(item: MenuItem, canShowIcon: Boolean = false, canShowKe canShowIcon = canShowIcon, canShowKeybinding = canShowKeybinding, iconKey = item.iconKey, - iconClass = item.iconClass, keybinding = item.keybinding, content = item.content, ) @@ -197,8 +197,7 @@ private fun ShowMenuItem(item: MenuItem, canShowIcon: Boolean = false, canShowKe enabled = item.isEnabled, submenu = item.submenu, canShowIcon = canShowIcon, - iconResource = item.iconResource, - iconClass = item.iconClass, + iconKey = item.iconKey, content = item.content, ) @@ -210,7 +209,6 @@ public interface MenuScope { public fun selectableItem( selected: Boolean, iconKey: IconKey? = null, - iconClass: Class<*>? = iconKey?.let { it::class.java }, keybinding: Set? = null, onClick: () -> Unit, enabled: Boolean = true, @@ -219,8 +217,7 @@ public interface MenuScope { public fun submenu( enabled: Boolean = true, - iconResource: String? = null, - iconClass: Class<*> = this::class.java, + iconKey: IconKey? = null, submenu: MenuScope.() -> Unit, content: @Composable () -> Unit, ) @@ -258,7 +255,6 @@ private fun (MenuScope.() -> Unit).asList() = buildList { override fun selectableItem( selected: Boolean, iconKey: IconKey?, - iconClass: Class<*>?, keybinding: Set?, onClick: () -> Unit, enabled: Boolean, @@ -269,7 +265,6 @@ private fun (MenuScope.() -> Unit).asList() = buildList { isSelected = selected, isEnabled = enabled, iconKey = iconKey, - iconClass = iconClass, keybinding = keybinding, onClick = onClick, content = content, @@ -283,12 +278,11 @@ private fun (MenuScope.() -> Unit).asList() = buildList { override fun submenu( enabled: Boolean, - iconResource: String?, - iconClass: Class<*>, + iconKey: IconKey?, submenu: MenuScope.() -> Unit, content: @Composable () -> Unit, ) { - add(SubmenuItem(enabled, iconResource, iconClass, submenu, content)) + add(SubmenuItem(enabled, iconKey, submenu, content)) } } ) @@ -302,7 +296,6 @@ private data class MenuSelectableItem( val isSelected: Boolean, val isEnabled: Boolean, val iconKey: IconKey?, - val iconClass: Class<*>?, val keybinding: Set?, val onClick: () -> Unit = {}, override val content: @Composable () -> Unit, @@ -312,8 +305,7 @@ private data class MenuPassiveItem(override val content: @Composable () -> Unit) private data class SubmenuItem( val isEnabled: Boolean = true, - val iconResource: String?, - val iconClass: Class<*>, + val iconKey: IconKey?, val submenu: MenuScope.() -> Unit, override val content: @Composable () -> Unit, ) : MenuItem @@ -341,7 +333,6 @@ internal fun MenuItem( modifier: Modifier = Modifier, enabled: Boolean = true, iconKey: IconKey?, - iconClass: Class<*>?, keybinding: Set?, canShowIcon: Boolean, canShowKeybinding: Boolean, @@ -421,12 +412,7 @@ internal fun MenuItem( if (canShowIcon) { val iconModifier = Modifier.size(style.metrics.itemMetrics.iconSize) if (iconKey != null) { - Icon( - key = iconKey, - contentDescription = null, - iconClass = iconClass ?: iconKey.javaClass, - modifier = iconModifier, - ) + Icon(key = iconKey, contentDescription = null, modifier = iconModifier) } else { Box(modifier = iconModifier) } @@ -454,6 +440,14 @@ internal fun MenuItem( } } +@Deprecated( + "Use the IconKey variant", + ReplaceWith( + "MenuSubmenuItem(modifier, enabled, canShowIcon, iconResource?.let { PathIconKey(it, iconClass) }, " + + "interactionSource, style, submenu, content)", + "org/jetbrains/jewel/ui/component/Menu.kt:472", + ), +) @Composable public fun MenuSubmenuItem( modifier: Modifier = Modifier, @@ -465,6 +459,21 @@ public fun MenuSubmenuItem( style: MenuStyle = JewelTheme.menuStyle, submenu: MenuScope.() -> Unit, content: @Composable () -> Unit, +) { + val iconKey = remember(iconResource, iconClass) { iconResource?.let { PathIconKey(it, iconClass) } } + MenuSubmenuItem(modifier, enabled, canShowIcon, iconKey, interactionSource, style, submenu, content) +} + +@Composable +public fun MenuSubmenuItem( + modifier: Modifier = Modifier, + enabled: Boolean = true, + canShowIcon: Boolean, + iconKey: IconKey?, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: MenuStyle = JewelTheme.menuStyle, + submenu: MenuScope.() -> Unit, + content: @Composable () -> Unit, ) { var itemState by remember(interactionSource) { mutableStateOf(MenuItemState.of(selected = false, enabled = enabled)) } @@ -525,8 +534,8 @@ public fun MenuSubmenuItem( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { if (canShowIcon) { - if (iconResource != null) { - Icon(resource = iconResource, iconClass = iconClass, contentDescription = "") + if (iconKey != null) { + Icon(key = iconKey, contentDescription = null) } else { Box(Modifier.size(style.metrics.itemMetrics.iconSize)) } From 51a313775f2c04c4ed1855155150e7eed20eadf1 Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Thu, 5 Sep 2024 13:05:42 +0200 Subject: [PATCH 2/3] Update readme, fleshing out icon loading in particular --- README.md | 135 +++++---- .../standalone/view/markdown/JewelReadme.kt | 266 +++++++++++++----- 2 files changed, 272 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 5d98116b3..aeff5b512 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ Then, in your app's `build.gradle.kts`: ```kotlin plugins { - // Should align with the Kotlin and Compose dependencies in Jewel - kotlin("jvm") version "1.9.21" - id("org.jetbrains.compose") version "1.6.0-dev1440" + // MUST align with the Kotlin and Compose dependencies in Jewel + kotlin("jvm") version "..." + id("org.jetbrains.compose") version "..." } repositories { @@ -132,7 +132,8 @@ to work for some: https://github.com/romainguy/kotlin-explorer/blob/main/compose ## Dependencies matrix Jewel is in continuous development and we focus on supporting only the Compose version we use internally. -You can see the latest supported version in [libs.versions.toml](https://github.com/JetBrains/jewel/blob/main/gradle/libs.versions.toml). +You can see the latest supported version +in [libs.versions.toml](https://github.com/JetBrains/jewel/blob/main/gradle/libs.versions.toml). Different versions of Compose are not guaranteed to work with different versions of Jewel. @@ -199,7 +200,7 @@ as `[mainTag]-xxx`, and used to publish the artifacts for that major IJP version > until that dependency isn't leaked on the classpath by Studio anymore. You can look at how the > [Package Search](https://github.com/JetBrains/package-search-intellij-plugin) plugin implements shadowing. -### Int UI Standalone theme +## Int UI Standalone theme The standalone theme can be used in any Compose for Desktop app. You use it as a normal theme, and you can customise it to your heart's content. By default, it matches the official Int UI specs. @@ -223,7 +224,7 @@ IntUiTheme(isDark = false) { If you want more control over the theming, you can use other `IntUiTheme` overloads, like the standalone sample does. -#### Custom window decoration +### Custom window decoration The JetBrains Runtime allows windows to have a custom decoration instead of the regular title bar. @@ -238,10 +239,10 @@ To get an IntelliJ-like custom title bar, you need to pass the window decoration ```kotlin IntUiTheme( - theme = themeDefinition, - styling = ComponentStyling.default().decoratedWindow( - titleBarStyle = TitleBarStyle.light() - ), + theme = themeDefinition, + styling = ComponentStyling.default().decoratedWindow( + titleBarStyle = TitleBarStyle.light() + ), ) { DecoratedWindow( onCloseRequest = { exitApplication() }, @@ -251,7 +252,7 @@ IntUiTheme( } ``` -### Running on the IntelliJ Platform: the Swing bridge +## Running on the IntelliJ Platform: the Swing bridge Jewel includes a crucial element for proper integration with the IDE: a bridge between the Swing components — theme and LaF — and the Compose world. @@ -273,7 +274,7 @@ SwingBridgeTheme { } ``` -#### Supported IntelliJ Platform versions +### Supported IntelliJ Platform versions To use Jewel in the IntelliJ Platform, you should depend on the appropriate `jewel-ide-laf-bridge-*` artifact, which will bring in the necessary transitive dependencies. These are the currently supported versions of the IntelliJ Platform @@ -286,86 +287,94 @@ and the branch on which the corresponding bridge code lives: | 2023.3 | `releases/233` | | 2023.2 (**deprecated**) | `archived-releases/232` | | 2023.1 or older | **Not supported** | + For an example on how to set up an IntelliJ Plugin, you can refer to the [`ide-plugin` sample](samples/ide-plugin/build.gradle.kts). ## Icons -When building for IntelliJ Platform or a standalone app, you can use the key-based icon loading API that allows you to load icons in a cross-target way. - -### Icons from your resources -To load an icon, you can use the `Icon` composable and provide a `PathIconKey` with a resource path: +Loading icons is best done with the `Icon` composable, which offers a key-based API that is portable across bridge and +standalone modules. Icon keys implement the `IconKey` interface, which is then internally used to obtain a resource path +to load the icon from. ```kotlin -// Equivalent to the old path-based API -Icon(PathIconKey("icons/myIcon.svg"), contentDescription = "...") +Icon(key = MyIconKeys.myIcon, contentDescription = "My icon") ``` -#### Build your own IconsKeys file +### Loading icons from the IntelliJ Platform -If you want to benefit from a better experience, not having to deal with strings and keys everywhere, you can build your own `*IconsKeys` file. Look at our `AllIconsKeys` file as a reference. You can maintain it manually, or you can automatically generate it, depending on your needs. +If you want to load an IJ platform icon, you can use `AllIconsKeys`, which is generated from the `AllIcons` platform +file. When using this in an IJ plugin, make sure you are using a version of the Jewel library matching the platform +version, because icons are known to shift between major versions — and sometimes, minor versions, too. -### Icons from IntelliJ Platform -If you need to use standard IntelliJ Platform icons in your standalone app, such as those found in `AllIcons`, you will need a bit of setup to make sure that the icons are present on the classpath and can be loaded as resources. +To use icons from `AllIconsKeys` in an IJ plugin, you don't need to do anything, as the icons are in the classpath by +default. If you want to use icons in a standalone app, you'll need to make sure the icons you want are on the classpath. +You can either copy the necessary icons in your resources, matching exactly the path they have in the IDE, or you can +add a dependency to the `com.jetbrains.intellij.platform:icons` artifact, which contains all the icons that end up in +`AllIconsKeys`. The latter is the recommended approach, since it's easy and the icons don't take up much disk space. -Add this to your build script: +Add this to your **standalone app** build script: ```kotlin dependencies { - implementation("com.jetbrains.intellij.platform:icons:[ijpVersion]") - // ... + implementation("com.jetbrains.intellij.platform:icons:[ijpVersion]") + // ... } repositories { - // Choose either of these two, depending on whether you're using a stable IJP or not - maven("https://www.jetbrains.com/intellij-repository/releases") - maven("https://www.jetbrains.com/intellij-repository/snapshots") + // Choose either of these two, depending on whether you're using a stable IJP or not + maven("https://www.jetbrains.com/intellij-repository/releases") + maven("https://www.jetbrains.com/intellij-repository/snapshots") } ``` > [!NOTE] -> If you are targeting an IntelliJ plugin, you don't need this additional setup since the icons are provided by the platform itself. +> If you are targeting an IntelliJ plugin, you don't need this additional setup since the icons are provided by the +> platform itself. -Once the icons are on the classhpath can use the `PlatformIcon` composable: +### Loading your own icons -```kotlin -// For platform icons found in AllIconsse -PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup") -``` - -### Old UI and new UI icons +To access your own icons, you'll need to create and maintain the `IconKey`s for them. We found that the easiest way when +you have up to a few dozen icons is to manually create an icon keys holder, like the ones we have in our samples. If you +have many more, you should consider generating these holders, instead. -The right `IconKey` to use depends on whether an icon has Old UI and New UI variants, or not: +In your holders, you can choose which implementation of `IconKey` to use: +* If your icons do not need to change between old UI and new UI, you can use the simpler `PathIconKey` +* If your icons are different in old and new UI, you should use `IntelliJIconKey`, which accepts two paths, one per + variant +* If you have different needs, you can also implement your own version of `IconKey` -* `PathIconKey` represents an icon from the resources that has no Old and New UI variants -* `IntelliJIconKey` is similar, but provides paths for both the Old and New UI variants +### Painter hints -If you are targeting only the New UI, you can use either `PathIconKey` or `IntelliJIconKey` to load the icon. If you are targeting the Old UI, you need to use `IntelliJIconKey`. +Jewel has an API to influence the loading and drawing of icons, called `PainterHint`. `Icon` composables have overloads +that take zero, one or more `PainterHint`s that will be used to compute the end result that shows up on screen. -### Icon runtime patching +`PainterHint`s can change the icon path (by adding a prefix/suffix, or changing it completely), tweak the contents of an +image (SVG patching, XML patching, bitmap patching), add decorations (e.g., badges), or do nothing at all (`None`). We +have several types of built-in `PainterHint`s which should cover all needs; if you find some use case that is not yet +handled, please file a feature request and we'll evaluate it. -Jewel emulates the under-the-hood machinations that happen in the IntelliJ Platform when loading icons. Specifically, -the resource will be subject to some transformations before being loaded. - -For example, in the IDE, if New UI is active, the icon path may be replaced with a different one. Some key colors in SVG -icons will also be replaced based on the current theme. See -[the docs](https://plugins.jetbrains.com/docs/intellij/work-with-icons-and-images.html#new-ui-icons). - -Beyond that, even in standalone, Jewel will pick up icons with the appropriate dark/light variant for the current theme, -and for bitmap icons it will try to pick the 2x variants based on the `LocalDensity`. +Both standalone and bridge themes provide a default set of implicit `PainterHint`s, for example to implement runtime +patching, like the IDE does. You can also use `PainterHint`s to affect how an icon will be drawn, or to select a +specific icon file, based on some criteria (e.g., `Size`). If you have a _stateful_ icon, that is if you need to display different icons based on some state, you can use the -`PainterProvider.getPainter(PainterHint...)` overload. You can then use one of the state-mapping `PainterHint` to let +`Icon(..., hint)` and `Icon(..., hints)` overloads. You can then use one of the state-mapping `PainterHint` to let Jewel load the appropriate icon automatically: ```kotlin // myState implements SelectableComponentState and has a ToggleableState property -val myPainter by myPainterProvider.getPainter( +val indeterminateHint = if (myState.toggleableState == ToggleableState.Indeterminate) { IndeterminateHint } else { PainterHint.None - }, + } + +Icon( + key = myKey, + contentDescription = "My icon", + indeterminateHint, Selected(myState), Stateful(myState), ) @@ -383,7 +392,22 @@ Assuming the PainterProvider has a base path of `components/myIcon.svg`, Jewel w right path based on the state. If you want to learn more about this system, look at the `PainterHint` interface and its implementations. -### Fonts +Please look at the `PainterHint` implementations and our samples for further information. + +### Default icon runtime patching + +Jewel emulates the under-the-hood machinations that happen in the IntelliJ Platform when loading icons. Specifically, +the resource will be subject to some transformations before being loaded. This is built on the `PainterHint` API we +described above. + +For example, in the IDE, if New UI is active, the icon path may be replaced with a different one. Some key colors in SVG +icons will also be replaced based on the current theme. See +[the docs](https://plugins.jetbrains.com/docs/intellij/work-with-icons-and-images.html#new-ui-icons). + +Beyond that, even in standalone, Jewel will pick up icons with the appropriate dark/light variant for the current theme, +and for bitmap icons it will try to pick the 2x variants based on the `LocalDensity`. + +## Fonts To load a system font, you can obtain it by its family name: @@ -417,7 +441,7 @@ val myLogicalFamily = Font("Dialog").asComposeFontFamily() val myLabelFamily = JBFont.label().asComposeFontFamily() ``` -### Swing interoperability +## Swing interoperability As this is Compose for Desktop, you get a good degree of interoperability with Swing. To avoid glitches and z-order issues, you should enable the @@ -439,6 +463,7 @@ Here is a small selection of projects that use Compose for Desktop and Jewel: * [Package Search](https://github.com/JetBrains/package-search-intellij-plugin) (IntelliJ Platform plugin) * [Kotlin Explorer](https://github.com/romainguy/kotlin-explorer) (standalone app) +* New task-based Profiler UI in Android Studio Koala * ...and more to come! ## Need help? diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt index b49818137..4ac0e1f2c 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/JewelReadme.kt @@ -7,8 +7,6 @@ internal val JewelReadme = """ # Jewel: a Compose for Desktop theme -Jewel logo - Jewel aims at recreating the IntelliJ Platform's _New UI_ Swing Look and Feel in Compose for Desktop, providing a desktop-optimized theme and set of components. @@ -27,56 +25,119 @@ Jewel provides an implementation of the IntelliJ Platform themes that can be use application. Additionally, it has a Swing LaF Bridge that only works in the IntelliJ Platform (i.e., used to create IDE plugins), but automatically mirrors the current Swing LaF into Compose for a native-looking, consistent UI. +> [!TIP] +> If you want to learn more about Jewel and Compose for Desktop and why they're a great, modern solution for your +> desktop +> UI needs, check out [this talk](https://www.droidcon.com/2023/11/15/meet-jewelcreate-ide-plugins-in-compose/) by Jewel +> contributors Sebastiano and Chris. +> +> It covers why Compose is a viable choice, and an overview of the Jewel project, plus +> some real-life use cases. + ## Getting started -To use Jewel in your app, you only need to add the relevant dependency. There are two scenarios: standalone Compose for -Desktop app, and IntelliJ Platform plugin. +The first thing to add is the necessary Gradle plugins, including the Compose Multiplatform plugin. You need to add a +custom repository for it in `settings.gradle.kts`: -For now, Jewel artifacts aren't available on Maven Central. You need to add a custom Maven repository to your build: +```kotlin +pluginManagement { + repositories { + google() + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + mavenCentral() + } +} +``` + +Then, in your app's `build.gradle.kts`: ```kotlin +plugins { + // MUST align with the Kotlin and Compose dependencies in Jewel + kotlin("jvm") version "..." + id("org.jetbrains.compose") version "..." +} + repositories { maven("https://packages.jetbrains.team/maven/p/kpm/public/") // Any other repositories you need (e.g., mavenCentral()) } ``` -If you're writing a **standalone app**, then you should depend on the `int-ui-standalone` artifact: +> [!WARNING] +> If you use convention plugins to configure your project you might run into issues such as +> [this](https://github.com/JetBrains/compose-multiplatform/issues/3748). To solve it, make sure the +> plugins are only initialized once — for example, by declaring them in the root `build.gradle.kts` +> with `apply false`, and then applying them in all the submodules that need them. + +To use Jewel in your app, you only need to add the relevant dependency. There are two scenarios: standalone Compose for +Desktop app, and IntelliJ Platform plugin. + +If you're writing a **standalone app**, then you should depend on the latest `int-ui-standalone-*` artifact: ```kotlin dependencies { - implementation("org.jetbrains.jewel:jewel-int-ui-standalone:[jewel version]") + // See https://github.com/JetBrains/Jewel/releases for the release notes + implementation("org.jetbrains.jewel:jewel-int-ui-standalone-[latest platform version]:[jewel version]") // Optional, for custom decorated windows: - implementation("org.jetbrains.jewel:jewel-int-ui-decorated-window:[jewel version]") + implementation("org.jetbrains.jewel:jewel-int-ui-decorated-window-[latest platform version]:[jewel version]") + + // Do not bring in Material (we use Jewel) + implementation(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + } } ``` -For an **IntelliJ Platform plugin**, then you should depend on the appropriate `ide-laf-bridge` artifact: +For an **IntelliJ Platform plugin**, then you should depend on the appropriate `ide-laf-bridge-*` artifact: ```kotlin dependencies { + // See https://github.com/JetBrains/Jewel/releases for the release notes // The platform version is a supported major IJP version (e.g., 232 or 233 for 2023.2 and 2023.3 respectively) implementation("org.jetbrains.jewel:jewel-ide-laf-bridge-[platform version]:[jewel version]") + + // Do not bring in Material (we use Jewel) and Coroutines (the IDE has its own) + api(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.kotlinx") + } } ```
> [!TIP] -> -> -> -> -> If you want to learn more about Jewel and Compose for Desktop and why they're a great, modern solution for your -> desktop -> UI needs, check out [this talk](https://www.droidcon.com/2023/11/15/meet-jewelcreate-ide-plugins-in-compose/) by Jewel -> contributors Sebastiano and Chris. -> -> It covers why Compose is a viable choice, and an overview of the Jewel project, plus -> some real-life use cases.
+> It's easier to use version catalogs — you can use the Jewel +> [version catalog](https://github.com/JetBrains/jewel/blob/main/gradle/libs.versions.toml) as reference. -
+## Using ProGuard/obfuscation/minification + +Jewel doesn't officially support using ProGuard to minimize and/or obfuscate your code, and there is currently no plan +to. +That said, people are reporting successes in using it. Please note that there is no guarantee that it will keep working, +and you most definitely need to have some rules in place. We don't provide any official rule set, but these have been +known +to work for some: https://github.com/romainguy/kotlin-explorer/blob/main/compose-desktop.pro + +> [!IMPORTANT] +> We won't accept bug reports for issues caused by the use of ProGuard or similar tools. + +## Dependencies matrix + +Jewel is in continuous development and we focus on supporting only the Compose version we use internally. +You can see the latest supported version +in [libs.versions.toml](https://github.com/JetBrains/jewel/blob/main/gradle/libs.versions.toml). + +Different versions of Compose are not guaranteed to work with different versions of Jewel. + +The Compose Compiler version used is the latest compatible with the given Kotlin version. See +[here](https://developer.android.com/jetpack/androidx/releases/compose-compiler) for the Compose +Compiler release notes, which indicate the compatibility. + +The minimum supported Kotlin version is dictated by the minimum supported IntelliJ IDEA platform. ## Project structure @@ -101,7 +162,12 @@ The project is split in modules: * `int-ui-decorated-window` has a standalone version of the Int UI styling values for the custom window decoration that can be used in any Compose for Desktop app 6. `ide-laf-bridge` contains the Swing LaF bridge to use in IntelliJ Platform plugins (see more below) -7. `samples` contains the example apps, which showcase the available components: +7. `markdown` contains a few modules: + * `core` the core logic for parsing and rendering Markdown documents with Jewel, using GitHub-like styling + * `extension` contains several extensions to the base CommonMark specs that can be used to add more features + * `ide-laf-bridge-styling` contains the IntelliJ Platform bridge theming for the Markdown renderer + * `int-ui-standalone-styling` contains the standalone Int UI theming for the Markdown renderer +8. `samples` contains the example apps, which showcase the available components: * `standalone` is a regular CfD app, using the standalone theme definitions and custom window decoration * `ide-plugin` is an IntelliJ plugin that showcases the use of the Swing Bridge @@ -124,7 +190,13 @@ as `[mainTag]-xxx`, and used to publish the artifacts for that major IJP version > We only support the latest build of IJP for each major IJP version. If the latest 233 version is 2023.3.3, for > example, we will only guarantee that Jewel works on that. Versions 2023.3.0–2023.3.2 might or might not work. -### Int UI Standalone theme +> [!CAUTION] +> When you target Android Studio, you might encounter issues due to Studio shipping its own (older) version of Jewel +> and Compose for Desktop. If you want to target Android Studio, you'll need to shadow the CfD and Jewel dependencies +> until that dependency isn't leaked on the classpath by Studio anymore. You can look at how the +> [Package Search](https://github.com/JetBrains/package-search-intellij-plugin) plugin implements shadowing. + +## Int UI Standalone theme The standalone theme can be used in any Compose for Desktop app. You use it as a normal theme, and you can customise it to your heart's content. By default, it matches the official Int UI specs. @@ -148,12 +220,10 @@ IntUiTheme(isDark = false) { If you want more control over the theming, you can use other `IntUiTheme` overloads, like the standalone sample does. -#### Custom window decoration +### Custom window decoration The JetBrains Runtime allows windows to have a custom decoration instead of the regular title bar. -![A screenshot of the custom window decoration in the standalone sample](https://github.com/JetBrains/jewel/blob/main/art/docs/custom-chrome.png?raw=true) - The standalone sample app shows how to easily get something that looks like a JetBrains IDE; if you want to go _very_ custom, you only need to depend on the `decorated-window` module, which contains all the required primitives, but not the Int UI styling. @@ -163,12 +233,10 @@ To get an IntelliJ-like custom title bar, you need to pass the window decoration ```kotlin IntUiTheme( - themeDefinition, - componentStyling = { - themeDefinition.decoratedWindowComponentStyling( - titleBarStyle = TitleBarStyle.light() - ) - }, + theme = themeDefinition, + styling = ComponentStyling.default().decoratedWindow( + titleBarStyle = TitleBarStyle.light() + ), ) { DecoratedWindow( onCloseRequest = { exitApplication() }, @@ -178,7 +246,7 @@ IntUiTheme( } ``` -### Running on the IntelliJ Platform: the Swing bridge +## Running on the IntelliJ Platform: the Swing bridge Jewel includes a crucial element for proper integration with the IDE: a bridge between the Swing components — theme and LaF — and the Compose world. @@ -200,73 +268,107 @@ SwingBridgeTheme { } ``` -#### Supported IntelliJ Platform versions +### Supported IntelliJ Platform versions To use Jewel in the IntelliJ Platform, you should depend on the appropriate `jewel-ide-laf-bridge-*` artifact, which will bring in the necessary transitive dependencies. These are the currently supported versions of the IntelliJ Platform and the branch on which the corresponding bridge code lives: -| IntelliJ Platform version(s) | Branch to use | - |------------------------------|-------------------| -| 2024.1 (EAP 3+) | `main` | -| 2023.3 | `releases/233` | -| 2023.2 | `releases/232` | -| 2023.1 or older | **Not supported** | +| IntelliJ Platform version(s) | Branch to use | +|------------------------------|-------------------------| +| 2024.2 (beta 1+) | `main` | +| 2024.1 (EAP 3+) | `releases/241` | +| 2023.3 | `releases/233` | +| 2023.2 (**deprecated**) | `archived-releases/232` | +| 2023.1 or older | **Not supported** | For an example on how to set up an IntelliJ Plugin, you can refer to the [`ide-plugin` sample](https://github.com/JetBrains/jewel/blob/main/samples/ide-plugin/build.gradle.kts). -#### Accessing icons +## Icons -When you want to draw an icon from the resources, you can either use the `Icon` composable and pass it the resource path -and the corresponding class to look up the classpath from, or go one lever deeper and use the lower level, -`Painter`-based API. - -The `Icon` approach looks like this: +Loading icons is best done with the `Icon` composable, which offers a key-based API that is portable across bridge and +standalone modules. Icon keys implement the `IconKey` interface, which is then internally used to obtain a resource path +to load the icon from. ```kotlin -// Load the "close" icon from the IDE's AllIcons class -Icon( - "actions/close.svg", - iconClass = AllIcons::class.java, - contentDescription = "Close", -) +Icon(key = MyIconKeys.myIcon, contentDescription = "My icon") ``` -To obtain a `Painter`, instead, you'd use: +### Loading icons from the IntelliJ Platform + +If you want to load an IJ platform icon, you can use `AllIconsKeys`, which is generated from the `AllIcons` platform +file. When using this in an IJ plugin, make sure you are using a version of the Jewel library matching the platform +version, because icons are known to shift between major versions — and sometimes, minor versions, too. + +To use icons from `AllIconsKeys` in an IJ plugin, you don't need to do anything, as the icons are in the classpath by +default. If you want to use icons in a standalone app, you'll need to make sure the icons you want are on the classpath. +You can either copy the necessary icons in your resources, matching exactly the path they have in the IDE, or you can +add a dependency to the `com.jetbrains.intellij.platform:icons` artifact, which contains all the icons that end up in +`AllIconsKeys`. The latter is the recommended approach, since it's easy and the icons don't take up much disk space. + +Add this to your **standalone app** build script: ```kotlin -val painterProvider = rememberResourcePainterProvider( - path = "actions/close.svg", - iconClass = AllIcons::class.java -) -val painter by painterProvider.getPainter() +dependencies { + implementation("com.jetbrains.intellij.platform:icons:[ijpVersion]") + // ... +} + +repositories { + // Choose either of these two, depending on whether you're using a stable IJP or not + maven("https://www.jetbrains.com/intellij-repository/releases") + maven("https://www.jetbrains.com/intellij-repository/snapshots") +} ``` -#### Icon runtime patching +> [!NOTE] +> If you are targeting an IntelliJ plugin, you don't need this additional setup since the icons are provided by the +> platform itself. -Jewel emulates the under-the-hood machinations that happen in the IntelliJ Platform when loading icons. Specifically, -the resource will be subject to some transformations before being loaded. +### Loading your own icons -For example, in the IDE, if New UI is active, the icon path may be replaced with a different one. Some key colors in SVG -icons will also be replaced based on the current theme. See -[the docs](https://plugins.jetbrains.com/docs/intellij/work-with-icons-and-images.html#new-ui-icons). +To access your own icons, you'll need to create and maintain the `IconKey`s for them. We found that the easiest way when +you have up to a few dozen icons is to manually create an icon keys holder, like the ones we have in our samples. If you +have many more, you should consider generating these holders, instead. -Beyond that, even in standalone, Jewel will pick up icons with the appropriate dark/light variant for the current theme, -and for bitmap icons it will try to pick the 2x variants based on the `LocalDensity`. +In your holders, you can choose which implementation of `IconKey` to use: +* If your icons do not need to change between old UI and new UI, you can use the simpler `PathIconKey` +* If your icons are different in old and new UI, you should use `IntelliJIconKey`, which accepts two paths, one per + variant +* If you have different needs, you can also implement your own version of `IconKey` + +### Painter hints + +Jewel has an API to influence the loading and drawing of icons, called `PainterHint`. `Icon` composables have overloads +that take zero, one or more `PainterHint`s that will be used to compute the end result that shows up on screen. + +`PainterHint`s can change the icon path (by adding a prefix/suffix, or changing it completely), tweak the contents of an +image (SVG patching, XML patching, bitmap patching), add decorations (e.g., badges), or do nothing at all (`None`). We +have several types of built-in `PainterHint`s which should cover all needs; if you find some use case that is not yet +handled, please file a feature request and we'll evaluate it. + +Both standalone and bridge themes provide a default set of implicit `PainterHint`s, for example to implement runtime +patching, like the IDE does. You can also use `PainterHint`s to affect how an icon will be drawn, or to select a +specific icon file, based on some criteria (e.g., `Size`). If you have a _stateful_ icon, that is if you need to display different icons based on some state, you can use the -`PainterProvider.getPainter(PainterHint...)` overload. You can then use one of the state-mapping `PainterHint` to let +`Icon(..., hint)` and `Icon(..., hints)` overloads. You can then use one of the state-mapping `PainterHint` to let Jewel load the appropriate icon automatically: ```kotlin // myState implements SelectableComponentState and has a ToggleableState property -val myPainter by myPainterProvider.getPainter( +val indeterminateHint = if (myState.toggleableState == ToggleableState.Indeterminate) { IndeterminateHint } else { PainterHint.None - }, + } + +Icon( + key = myKey, + contentDescription = "My icon", + indeterminateHint, Selected(myState), Stateful(myState), ) @@ -284,7 +386,22 @@ Assuming the PainterProvider has a base path of `components/myIcon.svg`, Jewel w right path based on the state. If you want to learn more about this system, look at the `PainterHint` interface and its implementations. -### Fonts +Please look at the `PainterHint` implementations and our samples for further information. + +### Default icon runtime patching + +Jewel emulates the under-the-hood machinations that happen in the IntelliJ Platform when loading icons. Specifically, +the resource will be subject to some transformations before being loaded. This is built on the `PainterHint` API we +described above. + +For example, in the IDE, if New UI is active, the icon path may be replaced with a different one. Some key colors in SVG +icons will also be replaced based on the current theme. See +[the docs](https://plugins.jetbrains.com/docs/intellij/work-with-icons-and-images.html#new-ui-icons). + +Beyond that, even in standalone, Jewel will pick up icons with the appropriate dark/light variant for the current theme, +and for bitmap icons it will try to pick the 2x variants based on the `LocalDensity`. + +## Fonts To load a system font, you can obtain it by its family name: @@ -310,15 +427,15 @@ API: ```kotlin val myAwtFamily = myFont.asComposeFontFamily() -// This will attempt to resolve the logical AWT font +// This will attempt to resolve the logical AWT font val myLogicalFamily = Font("Dialog").asComposeFontFamily() -// This only works in the IntelliJ Platform, +// This only works in the IntelliJ Platform, // since JBFont is only available there val myLabelFamily = JBFont.label().asComposeFontFamily() ``` -### Swing interoperability +## Swing interoperability As this is Compose for Desktop, you get a good degree of interoperability with Swing. To avoid glitches and z-order issues, you should enable the @@ -340,6 +457,7 @@ Here is a small selection of projects that use Compose for Desktop and Jewel: * [Package Search](https://github.com/JetBrains/package-search-intellij-plugin) (IntelliJ Platform plugin) * [Kotlin Explorer](https://github.com/romainguy/kotlin-explorer) (standalone app) +* New task-based Profiler UI in Android Studio Koala * ...and more to come! ## Need help? @@ -367,5 +485,5 @@ 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. ``` - """ +""" .trimIndent() From ed914952768e814374ce3b54d36d6b73ebfb69d5 Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Thu, 5 Sep 2024 13:06:48 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Deprecate=20PlatformIcon=20=E2=80=94=20not?= =?UTF-8?q?=20needed=20anymore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IconKey now carries the class information with itself, so no need for the composable anymore, whose job was to provide a default class. --- .../org/jetbrains/jewel/ui/component/PlatformIcon.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PlatformIcon.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PlatformIcon.kt index 7309a54ff..9aff22595 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PlatformIcon.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PlatformIcon.kt @@ -3,9 +3,15 @@ package org.jetbrains.jewel.ui.component import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval import org.jetbrains.jewel.ui.icon.IntelliJIconKey import org.jetbrains.jewel.ui.painter.PainterHint +@Deprecated( + "Use Icon directly, this doesn't have any advantage over it anymore.", + ReplaceWith("Icon(key, contentDescription, modifier, tint, hint)", "com.jewel.ui.component.Icon"), +) +@ScheduledForRemoval(inVersion = "Before 1.0") @Composable public fun PlatformIcon( key: IntelliJIconKey, @@ -17,6 +23,11 @@ public fun PlatformIcon( PlatformIcon(key, contentDescription, modifier, tint, *arrayOf(hint)) } +@Deprecated( + "Use Icon directly, this doesn't have any advantage over it anymore.", + ReplaceWith("Icon(key, contentDescription, modifier, tint, hints)", "com.jewel.ui.component.Icon"), +) +@ScheduledForRemoval(inVersion = "Before 1.0") @Composable public fun PlatformIcon( key: IntelliJIconKey,