Jewel: Compose for Desktop主题
Jewel旨在以Compose for Desktop重新创建IntelliJ平台的"新UI"Swing外观和感觉,提供针对桌面优化的主题和组件集。
[!警告]
该项目正在积极开发中,在考虑用于生产用途时建议谨慎。您可以使用它,但应该预期API会经常变更,东西可能会移动和/或损坏,以及所有这些常见情况。不保证跨版本的二进制兼容性,API仍在变化中且可能会更改。
目前,IntelliJ平台不正式支持使用Compose for Desktop编写第三方IntelliJ插件。它应该可以工作,但您的体验可能会有所不同,如果出现问题,您将无法获得支持。
使用时风险自负!
Jewel提供了可在任何Compose for Desktop应用程序中使用的IntelliJ平台主题实现。此外,它还有一个Swing LaF桥接器,只能在IntelliJ平台中使用(即用于创建IDE插件),但会自动将当前的Swing LaF镜像到Compose中,以获得原生外观的一致UI。
如果您想了解更多关于Jewel和Compose for Desktop的信息,以及为什么它们是满足您桌面UI需求的出色现代解决方案,请查看Jewel贡献者Sebastiano和Chris的这个演讲。
它涵盖了为什么Compose是一个可行的选择,以及Jewel项目的概述,还有一些真实世界的使用案例。
入门指南
首先要添加必要的Gradle插件,包括Compose Multiplatform插件。你需要在settings.gradle.kts
中添加一个自定义仓库:
pluginManagement {
repositories {
google()
gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
mavenCentral()
}
}
然后,在你的应用的build.gradle.kts
中:
plugins {
// 应与Jewel中的Kotlin和Compose依赖保持一致
kotlin("jvm") version "1.9.21"
id("org.jetbrains.compose") version "1.6.0-dev1440"
}
repositories {
maven("https://packages.jetbrains.team/maven/p/kpm/public/")
// 你需要的任何其他仓库(例如 mavenCentral())
}
[!警告] 如果你使用约定插件来配置项目,可能会遇到诸如这样的问题。要解决它,请确保插件只初始化一次 — 例如,在根
build.gradle.kts
中用apply false
声明它们,然后在所有需要它们的子模块中应用它们。
要在你的应用中使用Jewel,你只需添加相关的依赖项。有两种情况:独立的Compose for Desktop应用,和IntelliJ Platform插件。
如果你正在编写独立应用,那么你应该依赖最新的int-ui-standalone-*
构件:
dependencies {
// 查看 https://github.com/JetBrains/Jewel/releases 获取发布说明
implementation("org.jetbrains.jewel:jewel-int-ui-standalone-[最新平台版本]:[jewel版本]")
// 可选,用于自定义装饰窗口:
implementation("org.jetbrains.jewel:jewel-int-ui-decorated-window-[最新平台版本]:[jewel版本]")
// 不要引入Material(我们使用Jewel)
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
}
对于IntelliJ Platform插件,你应该依赖适当的ide-laf-bridge-*
构件:
dependencies {
// 查看 https://github.com/JetBrains/Jewel/releases 获取发布说明
// 平台版本是支持的主要IJP版本(例如,232或233分别代表2023.2和2023.3)
implementation("org.jetbrains.jewel:jewel-ide-laf-bridge-[平台版本]:[jewel版本]")
// 不要引入Material(我们使用Jewel)和Coroutines(IDE有自己的)
api(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
exclude(group = "org.jetbrains.kotlinx")
}
}
[!提示] 使用版本目录更容易 — 你可以使用Jewel的版本目录作为参考。
使用ProGuard/混淆/最小化
Jewel不官方支持使用ProGuard来最小化和/或混淆你的代码,目前也没有这样的计划。 话虽如此,人们报告在使用它时取得了成功。请注意,不能保证它会继续有效, 而且你肯定需要制定一些规则。我们不提供任何官方规则集,但这些规则已知 对某些人有效: https://github.com/romainguy/kotlin-explorer/blob/main/compose-desktop.pro
[!重要] 我们不会接受因使用ProGuard或类似工具而导致的问题的错误报告。
依赖矩阵
Jewel正在持续开发中,我们专注于支持我们内部使用的Compose版本。 你可以在libs.versions.toml中看到最新支持的版本。
不同版本的Compose不能保证与不同版本的Jewel一起工作。
使用的Compose Compiler版本是与给定Kotlin版本兼容的最新版本。请查看 这里的Compose Compiler发布说明,其中指出了兼容性。
支持的最低Kotlin版本由支持的最低IntelliJ IDEA平台决定。
项目结构
该项目分为多个模块:
buildSrc
包含构建逻辑,包括:jewel
和jewel-publish
配置插件jewel-check-public-api
和jewel-linting
配置插件- 主题调色板生成器插件
- Studio Releases生成器插件
foundation
包含基础的Jewel功能:- 没有强烈样式的基本组件(例如,
SelectableLazyColumn
,BasicLazyTree
) - 带有几个基本组合局部变量的
JewelTheme
接口 - 状态管理原语
- Jewel注解
- 其他一些原语
- 没有强烈样式的基本组件(例如,
ui
包含所有样式化组件和自定义画家逻辑decorated-window
包含在JetBrains Runtime上进行自定义窗口装饰的基本、无样式功能int-ui
包含两个模块:int-ui-standalone
有一个可以在任何Compose for Desktop应用中使用的Int UI样式值的独立版本int-ui-decorated-window
有一个可以在任何Compose for Desktop应用中使用的自定义窗口装饰的Int UI样式值的独立版本
ide-laf-bridge
包含用于IntelliJ Platform插件的Swing LaF桥(详见下文)markdown
包含几个模块:core
使用类似GitHub的样式,用Jewel解析和渲染Markdown文档的核心逻辑extension
包含几个可用于添加更多功能的基本CommonMark规范的扩展ide-laf-bridge-styling
包含Markdown渲染器的IntelliJ Platform桥主题int-ui-standalone-styling
包含Markdown渲染器的独立Int UI主题
samples
包含展示可用组件的示例应用:standalone
是一个常规CfD应用,使用独立主题定义和自定义窗口装饰ide-plugin
是一个展示如何使用Swing桥的IntelliJ插件
分支策略和IJ平台
主分支上的代码是针对当前最新的IntelliJ Platform版本开发和测试的。
当新的主要版本的EAP开始时,我们会切出一个releases/xxx
发布分支,其中xxx
是跟踪的主要
IJP版本。此时,主分支开始跟踪最新可用的主要IJP版本,并根据需要将更改cherry-pick到每个发布分支。
所有活跃的发布分支都具有相同的功能(在相应的IJP版本支持的情况下),但可能在平台版本特定的修复和内部实现上有所不同。
独立的Int UI主题将始终以与最新主要IJP版本相同的方式工作;发布分支将不
包括int-ui
模块,该模块始终从主分支发布。
Jewel的发布总是从主分支上的标签切出;然后每个releases/xxx
分支的HEAD被标记为
[mainTag]-xxx
,并用于发布该主要IJP版本的构件。
[!重要] 我们只支持每个主要IJP版本的最新构建。例如,如果最新的233版本是2023.3.3, 我们只保证Jewel在该版本上工作。2023.3.0–2023.3.2版本可能有效,也可能无效。
[!注意] 当你针对Android Studio时,你可能会遇到问题,因为Studio附带了自己的(较旧的)Jewel 和Compose for Desktop版本。如果你想针对Android Studio,在Studio不再在类路径上泄露该依赖之前, 你需要对CfD和Jewel依赖进行阴影处理。你可以查看Package Search插件如何实现阴影处理。
Int UI独立主题
独立主题可以在任何Compose for Desktop应用中使用。你可以像使用普通主题一样使用它,并且可以随心所欲地自定义它。 默认情况下,它与官方Int UI规范相匹配。
有关如何设置独立应用的示例,你可以参考standalone
示例。
[!警告] 请注意,Jewel需要JetBrains Runtime才能正确工作。一些功能如字体加载依赖于它, 因为它具有其他JDK中不可用的额外功能和UI功能补丁。 我们不支持在任何其他JDK上运行Jewel。
要在非IntelliJ Platform环境中使用Jewel组件,你需要将UI层次结构包装在IntUiTheme
可组合函数中:
IntUiTheme(isDark = false) {
// ...
}
如果你想对主题有更多控制,可以使用其他IntUiTheme
重载,就像独立示例所做的那样。
自定义窗口装饰
JetBrains Runtime允许窗口具有自定义装饰,而不是常规标题栏。
独立示例应用展示了如何轻松获得类似JetBrains IDE的外观;如果你想进行非常
自定义,你只需依赖decorated-window
模块,其中包含所有必需的原语,但不包含Int UI样式。
要获得类似IntelliJ的自定义标题栏,你需要将窗口装饰样式传递给主题调用,并在主题的顶层
添加DecoratedWindow
可组合函数:
IntUiTheme(
theme = themeDefinition,
styling = ComponentStyling.default().decoratedWindow(
titleBarStyle = TitleBarStyle.light()
),
) {
DecoratedWindow(
onCloseRequest = { exitApplication() },
) {
// ...
}
}
在IntelliJ Platform上运行:Swing桥
Jewel包含一个与IDE正确集成的关键元素:Swing组件 — 主题和LaF — 与Compose世界之间的桥梁。
这个桥梁确保我们获取当前IntelliJ主题中定义的颜色、排版、度量和图像,并将它们应用到Compose组件中。 这意味着Jewel将自动适应使用标准主题 机制的IntelliJ Platform主题。
[!注意] 使用非标准机制(例如为Swing组件提供自定义UI实现)的IntelliJ主题不受支持,也永远不会受支持。
如果你正在编写IntelliJ Platform插件,你应该使用SwingBridgeTheme
而不是独立主题:
SwingBridgeTheme {
// ...
}
支持的IntelliJ Platform版本
要在IntelliJ Platform中使用Jewel,你应该依赖适当的jewel-ide-laf-bridge-*
构件,
它将带来必要的传递依赖。以下是当前支持的IntelliJ Platform版本和相应桥接代码所在的分支:
IntelliJ Platform 版本 | 使用的分支 |
---|---|
2024.2 (beta 1+) | main |
2024.1 (EAP 3+) | releases/241 |
2023.3 | releases/233 |
2023.2 (已弃用) | archived-releases/232 |
2023.1 或更早版本 | 不支持 |
关于如何设置 IntelliJ 插件的示例,你可以参考 ide-plugin
样例。
图标
在为 IntelliJ Platform 或独立应用构建时,你可以使用基于键的图标加载 API,它允许你以跨目标的方式加载图标。
从你的资源加载图标
要加载图标,你可以使用 Icon
可组合函数并提供带有资源路径的 PathIconKey
:
// 等同于旧的基于路径的 API
Icon(PathIconKey("icons/myIcon.svg"), contentDescription = "...")
构建你自己的 IconsKeys 文件
如果你想获得更好的体验,不必到处处理字符串和键,你可以构建自己的 *IconsKeys
文件。以我们的 AllIconsKeys
文件为参考。你可以手动维护它,或者根据需要自动生成它。
从 IntelliJ Platform 加载图标
如果你需要在独立应用中使用标准的 IntelliJ Platform 图标,比如那些在 AllIcons
中找到的图标,你需要进行一些设置以确保图标存在于类路径中并可以作为资源加载。
在你的构建脚本中添加以下内容:
dependencies {
implementation("com.jetbrains.intellij.platform:icons:[ijpVersion]")
// ...
}
repositories {
// 根据你是否使用稳定版的 IJP 选择以下两者之一
maven("https://www.jetbrains.com/intellij-repository/releases")
maven("https://www.jetbrains.com/intellij-repository/snapshots")
}
[!注意] 如果你的目标是 IntelliJ 插件,你不需要这个额外的设置,因为平台本身提供了图标。
一旦图标在类路径上,你就可以使用 PlatformIcon
可组合函数:
// 对于在 AllIcons 中找到的平台图标
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup")
旧 UI 和新 UI 图标
要使用的正确 IconKey
取决于图标是否有旧 UI 和新 UI 变体:
PathIconKey
表示来自资源的没有旧 UI 和新 UI 变体的图标IntelliJIconKey
类似,但为旧 UI 和新 UI 变体提供路径
如果你只针对新 UI,你可以使用 PathIconKey
或 IntelliJIconKey
来加载图标。如果你针对旧 UI,你需要使用 IntelliJIconKey
。
图标运行时修补
Jewel 模拟了 IntelliJ Platform 在加载图标时幕后进行的操作。具体来说,资源在加载之前会经过一些转换。
例如,在 IDE 中,如果新 UI 处于活动状态,图标路径可能会被替换为不同的路径。SVG 图标中的一些关键颜色也会根据当前主题进行替换。参见文档。
除此之外,即使在独立应用中,Jewel 也会根据当前主题选择适当的深色/浅色变体图标,对于位图图标,它会根据 LocalDensity
尝试选择 2x 变体。
如果你有一个_有状态的_图标,也就是说,如果你需要根据某些状态显示不同的图标,你可以使用 PainterProvider.getPainter(PainterHint...)
重载。然后你可以使用一个状态映射 PainterHint
让 Jewel 自动加载适当的图标:
// myState 实现 SelectableComponentState 并有一个 ToggleableState 属性
val myPainter by myPainterProvider.getPainter(
if (myState.toggleableState == ToggleableState.Indeterminate) {
IndeterminateHint
} else {
PainterHint.None
},
Selected(myState),
Stateful(myState),
)
其中 IndeterminateHint
如下所示:
private object IndeterminateHint : PainterSuffixHint() {
override fun suffix(): String = "Indeterminate"
}
假设 PainterProvider 的基本路径是 components/myIcon.svg
,Jewel 将根据状态自动将其转换为正确的路径。如果你想了解更多关于这个系统的信息,请查看 PainterHint
接口及其实现。
字体
要加载系统字体,你可以通过其家族名称获取:
val myFamily = FontFamily("My Family")
如果你想使用 JetBrains Runtime 中嵌入的字体,你可以使用 EmbeddedFontFamily
API:
import javax.swing.text.StyledEditorKit.FontFamilyAction
// 如果 JBR 中不存在匹配的字体家族,将返回 null
val myEmbeddedFamily = EmbeddedFontFamily("Embedded family")
// 在处理嵌入式字体时,建议加载一个后备字体家族
val myFamily = myEmbeddedFamily ?: FontFamily("Fallback family")
你可以通过使用 asComposeFontFamily()
API 从任何 java.awt.Font
(包括 JBFont
)获取 FontFamily
:
val myAwtFamily = myFont.asComposeFontFamily()
// 这将尝试解析逻辑 AWT 字体
val myLogicalFamily = Font("Dialog").asComposeFontFamily()
// 这只在 IntelliJ Platform 中有效,
// 因为 JBFont 只在那里可用
val myLabelFamily = JBFont.label().asComposeFontFamily()
Swing 互操作性
由于这是 Compose for Desktop,你可以获得与 Swing 的良好互操作性。为了避免故障和 z 顺序问题,你应该在初始化 Compose 内容之前启用实验性 Swing 渲染管道。
ide-laf-bridge
模块提供的 ToolWindow.addComposeTab()
扩展函数会为你处理这个问题。但是,如果你也想在其他场景和独立应用中启用它,你可以在 Compose 入口点(即在创建 ComposePanel
之前)调用 enableNewSwingCompositing()
函数。
[!注意] 新的 Swing 渲染管道是实验性的,在使用无限重复动画时可能会影响性能。这是 Compose Multiplatform 团队已知的问题,需要在 Java 运行时中进行更改才能解决。一旦在 JetBrains Runtime 中进行了必要的更改,我们将删除此通知。
使用 Jewel 编写的项目
以下是一些使用 Compose for Desktop 和 Jewel 的项目示例:
- Package Search(IntelliJ Platform 插件)
- Kotlin Explorer(独立应用)
- ...更多即将到来!
需要帮助?
你可以在 Kotlin Slack 的 #jewel
频道寻求帮助。
如果你还没有访问 Kotlin Slack 的权限,可以在这里申请。
许可证
Jewel 根据 Apache 2.0 许可证 授权。
Copyright 2022–4 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.