GitOrigin-RevId: ee785825e2070b33d9981932340eee4972035e3f
This commit is contained in:
Dmitriy.Panov
2024-11-29 21:19:51 +01:00
committed by intellij-monorepo-bot
710 changed files with 75711 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 150
tab_width = 4
[{*.html,*.js,*.css}]
indent_size = 2
[*.yml]
indent_size = 2
[{*.kt,*.kts}]
ij_kotlin_allow_trailing_comma = true
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_function_signature_body_expression_wrapping = multiline
ktlint_ignore_back_ticked_identifier = true
ktlint_standard_annotation = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_import-ordering = disabled
ktlint_standard_indent = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_try-catch-finally-spacing = disabled
ktlint_standard_wrapping = disabled
[gradlew.bat]
end_of_line = crlf
[{*.bash,*.sh,*.zsh}]
indent_size = 2
tab_width = 2
[*.md]
indent_size = 2

View File

@@ -0,0 +1,34 @@
name: CI checks
on:
push:
branches:
- main
- 'releases/**'
pull_request:
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: zulu
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run :check task
run: ./gradlew check --continue --no-daemon
# - uses: github/codeql-action/upload-sarif@v3
# if: ${{ always() }}
# with:
# sarif_file: ${{ github.workspace }}/build/reports/static-analysis.sarif
# checkout_path: ${{ github.workspace }}

View File

@@ -0,0 +1,36 @@
name: IJ Platform version updates
on:
workflow_dispatch:
schedule:
- cron: 0 8 * * *
jobs:
check-ij-platform-updates:
name: Check for IJP updates
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch-name:
- releases/241
- releases/242
- main
steps:
- uses: actions/checkout@v4
with:
ref: ${{ matrix.branch-name }}
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 21
cache: gradle
- name: Setup Gradle
run: chmod +x gradlew
- name: Run Gradle
run: ./gradlew checkLatestIntelliJPlatformBuild --no-daemon

View File

@@ -0,0 +1,35 @@
name: Publish hotfix artifacts in Space
on:
workflow_dispatch:
inputs:
tag-name:
description: 'Hotfix tag to publish'
required: true
jobs:
publish-current:
name: Publish hotfix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag-name }}
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17
cache: gradle
- name: Setup Gradle
run: chmod +x gradlew
- name: Run Gradle
run: ./gradlew publishMainPublicationToSpaceRepository --no-daemon
env:
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
PGP_PASSWORD: ${{secrets.PGP_PASSWORD}}
PGP_PRIVATE_KEY: ${{secrets.PGP_PRIVATE_KEY}}

View File

@@ -0,0 +1,67 @@
name: Publish artifacts in Space
on:
release:
types: [ published ]
push:
branches:
- main
- 'releases/**'
- 'archived-releases/**'
jobs:
publish-current:
name: Publish current IJP version (main)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 21
cache: gradle
- name: Setup Gradle
run: chmod +x gradlew
- name: Run Gradle
run: ./gradlew publishMainPublicationToSpaceRepository --no-daemon
env:
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
PGP_PASSWORD: ${{secrets.PGP_PASSWORD}}
PGP_PRIVATE_KEY: ${{secrets.PGP_PRIVATE_KEY}}
publish-older:
name: Publish older IJP version(s)
runs-on: ubuntu-latest
strategy:
matrix:
branch-name:
- releases/241
- releases/242
steps:
- uses: actions/checkout@v4
with:
ref: ${{ matrix.branch-name }}
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 17
cache: gradle
- name: Setup Gradle
run: chmod +x gradlew
- name: Run Gradle
run: ./gradlew publishMainPublicationToSpaceRepository --no-daemon
env:
MAVEN_SPACE_USERNAME: ${{secrets.MAVEN_SPACE_USERNAME}}
MAVEN_SPACE_PASSWORD: ${{secrets.MAVEN_SPACE_PASSWORD}}
PGP_PASSWORD: ${{secrets.PGP_PASSWORD}}
PGP_PRIVATE_KEY: ${{secrets.PGP_PRIVATE_KEY}}

View File

@@ -0,0 +1,34 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 6 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been inactive for a long time.'
stale-pr-message: 'Stale pull request message'
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-labels: 'blocked-externally'
exempt-pr-labels: 'blocked-externally'
only-labels: 'needs-info'
days-before-stale: 7
days-before-close: 3
labels-to-remove-when-unstale: 'stale'

109
platform/jewel/.gitignore vendored Normal file
View File

@@ -0,0 +1,109 @@
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
### Gradle template
.gradle
build/
### Terraform template
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
*.iml
*.ipr
*.iws
.idea/*
out/
local.properties
# IDEA/Android Studio project settings ignore exceptions
!.idea/codeStyles/
!.idea/copyright/
!.idea/dataSources.xml
!.idea/detekt.xml
!.idea/encodings.xml
!.idea/externalDependencies.xml
!.idea/fileTemplates/
!.idea/icon.svg
!.idea/icon.png
!.idea/icon_dark.png
!.idea/inspectionProfiles/
!.idea/ktfmt.xml
!.idea/ktlint.xml
!.idea/ktlint-plugin.xml
!.idea/runConfigurations/
!.idea/scopes/
!.idea/vcs.xml
### Kotlin template
# Compiled class file
*.class
# Log file
*.log
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Windows template
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### Misc
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Ignore IJP temp folder
/.intellijPlatform
# Ignore release patch generator output
/this-release.txt
# Ignore Kotlin compiler sessions
/.kotlin
/buildSrc/.kotlin

View File

@@ -0,0 +1,51 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="true" />
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="true" />
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="true" />
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="true" />
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="true" />
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="true" />
<option name="CONTINUATION_INDENT_IN_ELVIS" value="true" />
<option name="IF_RPAREN_ON_NEW_LINE" value="false" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="EXTENDS_LIST_WRAP" value="0" />
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
<option name="ASSIGNMENT_WRAP" value="5" />
<option name="METHOD_ANNOTATION_WRAP" value="5" />
<option name="CLASS_ANNOTATION_WRAP" value="5" />
<option name="FIELD_ANNOTATION_WRAP" value="5" />
<option name="PARAMETER_ANNOTATION_WRAP" value="5" />
<option name="VARIABLE_ANNOTATION_WRAP" value="5" />
<option name="ENUM_CONSTANTS_WRAP" value="5" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

17
platform/jewel/.idea/detekt.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DetektPluginSettings">
<option name="configurationFilePaths">
<set>
<option value="$PROJECT_DIR$/detekt.yml" />
</set>
</option>
<option name="configurationFiles">
<list>
<option value="$PROJECT_DIR$/detekt.yml" />
</list>
</option>
<option name="enableDetekt" value="true" />
<option name="enableForProjectResult" value="Accepted" />
</component>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalDependencies">
<plugin id="com.facebook.ktfmt_idea_plugin" min-version="1.2.0.52" />
<plugin id="com.nbadal.ktlint" />
<plugin id="detekt" />
<plugin id="org.jetbrains.compose.desktop.ide" />
<plugin id="org.jetbrains.kotlin" />
<plugin id="org.norbye.tor.kdocformatter" />
</component>
</project>

7
platform/jewel/.idea/icon.svg generated Normal file
View File

@@ -0,0 +1,7 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="black"/>
<rect x="256" y="76" width="254.558" height="254.558" transform="rotate(45 256 76)" fill="white"/>
<path d="M256 76L436 256H256V76Z" fill="#CCCCCC"/>
<path d="M76 256L256 436V256H76Z" fill="#CCCCCC"/>
<path d="M256 436L436 256H256V436Z" fill="#808080"/>
</svg>

After

Width:  |  Height:  |  Size: 427 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AntMissingPropertiesFileInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="DeprecatedMavenDependency" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigKeyCorrectness" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GradlePackageUpdate" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="HardwiredNamespacePrefix" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IndexZeroUsage" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavaFxEventHandler" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NoButtonGroup" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NoScrollPane" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StructuralWrap" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="XsltDeclarations" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="XsltUnusedDeclaration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="XsltVariableShadowing" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

11
platform/jewel/.idea/ktfmt.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KtfmtSettings">
<option name="customBlockIndent" value="4" />
<option name="customManageTrailingCommas" value="true" />
<option name="customMaxLineLength" value="120" />
<option name="enableKtfmt" value="Enabled" />
<option name="enabled" value="true" />
<option name="uiFormatterStyle" value="Custom" />
</component>
</project>

15
platform/jewel/.idea/ktlint-plugin.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KtLint plugin">
<ktlintMode>MANUAL</ktlintMode>
<externalJarPaths>
<list>
<option value="$PROJECT_DIR$/../ktlint-compose-0.4.11-all.jar" />
</list>
</externalJarPaths>
</component>
<component name="com.nbadal.ktlint.KtlintProjectSettings">
<ktlintMode>MANUAL</ktlintMode>
<attachToIntellijFormat>false</attachToIntellijFormat>
</component>
</project>

View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Check IJP updates" type="GradleRunConfiguration" factoryName="Gradle" folderName="Checks">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="checkLatestIntelliJPlatformBuild" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="IDE sample" type="GradleRunConfiguration" factoryName="Gradle" show_console_on_std_err="true" show_console_on_std_out="true">
<log_file alias="IDE Logs" path="$PROJECT_DIR$/samples/ide-plugin/build/idea-sandbox/IC-2024.2/log/idea.log" show_all="true" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/ide-plugin" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":samples:ide-plugin:runIde" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Pre-push" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="apiDump" />
<option value="ktfmtFormat" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Reformat project" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="ktfmtFormat" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Regenerate icon keys" type="GradleRunConfiguration" factoryName="Gradle" folderName="Generators">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--rerun-tasks" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":ui:generateAllIconsKeys" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Regenerate themes" type="GradleRunConfiguration" factoryName="Gradle" folderName="Generators">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--rerun-tasks" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":int-ui:int-ui-standalone:generateIntUiDarkTheme" />
<option value=":int-ui:int-ui-standalone:generateIntUiLightTheme" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run checks" type="GradleRunConfiguration" factoryName="Gradle" folderName="Checks">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="check" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Stand-alone sample" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/standalone" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Tag release" type="GradleRunConfiguration" factoryName="Gradle" folderName="Publish">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":tagRelease" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test publication" type="GradleRunConfiguration" factoryName="Gradle" folderName="Publish">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-PuseCurrentVersion -Pno-sign=true" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="cleanTestPublishArtifacts" />
<option value="publishAllPublicationsToLocalTestRepository" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

16
platform/jewel/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="BodyLimit" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SubjectBodySeparation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="GitSharedSettings">
<option name="synchronizeBranchProtectionRules" value="false" />
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[@jetbrains](https://github.com/jetbrains).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -0,0 +1,72 @@
# Contributions
We're glad to accept contributions from the community.
However, **before you open a PR**, please make sure there is a corresponding issue open.
In the case of a new feature, a large refactoring, and similar non-trivial changes, it's necessary that a project maintainer greenlights it
in advance on the issue page. Non pre-approved PRs that aren't bug fixes or trivial in nature will **not** be accepted.
# How to contribute
Prerequisites:
- Familiarity with [pull requests](https://help.github.com/articles/using-pull-requests) and [issues](https://guides.github.com/features/issues/).
- Knowledge of [Markdown](https://help.github.com/articles/markdown-basics/) for editing `.md` documents.
- Knowledge of [Kotlin](https://kotlinlang.org), [Jetpack Compose](https://d.android.com/jetpack/compose), and
[Compose Multiplatform](https://jetbrains.com/lp/compose)
In particular, this community seeks the following types of contributions:
- **Ideas**: participate in an issue thread or start your own to have your voice heard.
- **Bug fixes**: bugs are inevitable, fixes are always welcome.
- **Features**: see the [Contributions](#contributions) section above
# Conduct
We are committed to providing a friendly, safe and welcoming environment for
all, regardless of gender, sexual orientation, disability, ethnicity, religion,
or similar personal characteristic.
Please avoid using overtly sexual nicknames or other nicknames that
might detract from a friendly, safe and welcoming environment for all.
Please be kind and courteous. There's no need to be mean or rude.
Respect that people have differences of opinion and that every design or
implementation choice carries a trade-off and numerous costs. There is seldom
a right answer, merely an optimal answer given a set of values and
circumstances. Try to assume that our community members are nice people
and remember, [miscommunication happens](https://hiddenbrain.org/podcast/why-conversations-go-wrong/).
Please keep unstructured critique to a minimum. If you have solid ideas you
want to experiment with, make a fork and see how it works.
We will exclude you from interaction if you insult, demean or harass anyone.
That is not welcome behaviour. We interpret the term "harassment" as
including the definition in the [Code of Conduct](CODE_OF_CONDUCT.md);
if you have any lack of clarity about what might be included in that concept,
please read the definition. In particular, we don't tolerate behavior that
excludes people in socially marginalized groups.
Private harassment is also unacceptable. No matter who you are, if you feel
you have been or are being harassed or made uncomfortable by a community
member, please contact [one of the maintainers](https://github.com/JetBrains/jewel/graphs/contributors)
immediately.
Whether you're a regular contributor or a newcomer, we care about
making this community a safe place for you and we've got your back.
Likewise any spamming, trolling, flaming, baiting or other attention-stealing
behaviour is not welcome.
# Communication
GitHub issues, PRs and discussions are the primary way for communicating about specific proposed
changes to this project.
In all those contexts, please follow the conduct guidelines above. Language issues
are often contentious and we'd like to keep discussion brief, civil and focused
on what we're actually doing, not wandering off into too much imaginary stuff.
# Inspiration
This guide is inspired to [Juno Suárez' contribution guidelines](https://github.com/junosuarez/CONTRIBUTING.md/blob/master/CONTRIBUTING.md),
licensed under CC-0 license.

201
platform/jewel/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 20223 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.

514
platform/jewel/README.md Normal file
View File

@@ -0,0 +1,514 @@
[![JetBrains incubator](https://img.shields.io/badge/JetBrains-incubator-yellow?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMzIuMDAwMDEiIHZpZXdCb3g9IjAgMCAzMiAzMi4wMDAwMSIgd2lkdGg9IjMyIj48c2NyaXB0IHhtbG5zPSIiLz48cGF0aCBkPSJtMCAwaDMydjMyLjAwMDAxaC0zMnoiLz48cGF0aCBkPSJtNCAyNi4wMDAwMWgxMnYyaC0xMnoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=)](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?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHN0cm9rZT0iI0ZGRiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0ibTMgNiAzIDFtMCAwLTMgOWE1LjAwMiA1LjAwMiAwIDAgMCA2LjAwMSAwTTYgN2wzIDlNNiA3bDYtMm02IDIgMy0xbS0zIDEtMyA5YTUuMDAyIDUuMDAyIDAgMCAwIDYuMDAxIDBNMTggN2wzIDltLTMtOS02LTJtMC0ydjJtMCAxNlY1bTAgMTZIOW0zIDBoMyIvPjwvc3ZnPg==)](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) ![Compose for Desktop version](https://img.shields.io/badge/Compose%20for%20Desktop-1.6.0-dev1369?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgNjcgNzQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zNS45OTkgMi42NjNhNS4wMSA1LjAxIDAgMCAwLTQuOTk4IDBsLTI2LjUgMTUuMjUzYTQuOTk0IDQuOTk0IDAgMCAwLTEuMTk4Ljk2MmwxMS4xMDggNi4zNjZjLjI2OC0uMjkuNTgtLjU0LjkzMS0uNzQ0bDE2LjE1Ni05LjM0MmE0IDQgMCAwIDEgNC4wMDQgMEw1MS42NTcgMjQuNWMuMzUxLjIwMy42NjQuNDU1LjkzMi43NDRsMTEuMTA4LTYuMzY2YTQuOTkxIDQuOTkxIDAgMCAwLTEuMTk4LS45NjJsLTI2LjUtMTUuMjUzWm0yOC43MjMgMTcuOTMzLTExLjE4MyA2LjQwOGMuMDc2LjMxLjExNi42MzIuMTE2Ljk1OXYxNy43OTRhNCA0IDAgMCAxLTEuOTU4IDMuNDRsLTE2LjIzNSA5LjYzOGEzLjk5OCAzLjk5OCAwIDAgMS0uOTYyLjQxMnYxMi42M2E1LjAwNSA1LjAwNSAwIDAgMCAxLjQyOC0uNTY5bDI2LjYyLTE1LjczQTQuOTg2IDQuOTg2IDAgMCAwIDY1IDUxLjI4NFYyMi4yMzdjMC0uNTY3LS4wOTctMS4xMi0uMjc4LTEuNjRaTTIgMjIuMjM3YzAtLjU2Ny4wOTctMS4xMi4yNzgtMS42NGwxMS4xODMgNi40MDdjLS4wNzYuMzEtLjExNi42MzItLjExNi45NTl2MTguNjMzYTQgNCAwIDAgMCAyLjA4IDMuNTA5bDE2LjA3NCA4LjhjLjMyLjE3NC42NTYuMzAyIDEuMDAxLjM4NHYxMi42MzhhNS4wMDUgNS4wMDUgMCAwIDEtMS41MTctLjUzM0w0LjYwMyA1Ny4wMkE0Ljk4NyA0Ljk4NyAwIDAgMSAyIDUyLjY0MlYyMi4yMzdaTTMwLjAwMi45MzVhNy4wMTQgNy4wMTQgMCAwIDEgNi45OTYgMGwyNi41IDE1LjI1M0E2Ljk4IDYuOTggMCAwIDEgNjcgMjIuMjM4djI5LjA0N2E2Ljk4IDYuOTggMCAwIDEtMy40MzMgNi4wMDlsLTI2LjYyIDE1LjczMWE3LjAxNCA3LjAxNCAwIDAgMS02LjkyMy4xMkwzLjY0NCA1OC43NzFBNi45ODEgNi45ODEgMCAwIDEgMCA1Mi42NDFWMjIuMjM4YTYuOTggNi45OCAwIDAgMSAzLjUwMi02LjA1TDMwLjAwMi45MzZabS04LjYwNCAyNy41NTIgMTAuNTgyLTYuMTFjLjk0LS41NDIgMi4xLS41NDIgMy4wNCAwbDEwLjU4MiA2LjExYTIuOTk2IDIuOTk2IDAgMCAxIDEuNTAzIDIuNTkzdjExLjY1M2MwIDEuMDU2LS41NiAyLjAzNC0xLjQ3MyAyLjU3NmwtMTAuNjQzIDYuMzA4YTMuMDQ0IDMuMDQ0IDAgMCAxLTMuMDA5LjA1MmwtMTAuNTItNS43NWEyLjk5NiAyLjk5NiAwIDAgMS0xLjU2NS0yLjYyN1YzMS4wOGMwLTEuMDY4LjU3My0yLjA1NiAxLjUwMy0yLjU5M1oiIGZpbGw9IiNmZmYiLz48L3N2Zz4%3D)
# Jewel: a Compose for Desktop theme
<img alt="Jewel logo" src="art/jewel-logo.svg" width="20%"/>
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.
> [!CAUTION]
> Jewel is moving to the IntelliJ Platform! All active development will move to
> https://github.com/JetBrains/intellij-community and this repository will just mirror that.
> More information to follow soon — but please **consider the code on this repository as read-only**.
---
> [!WARNING]
>
> This project is in active development, and caution is advised when considering it for production uses. You _can_ use
> it, but you should expect APIs to change often, things to move around and/or break, and all that jazz. Binary
> compatibility is not guaranteed across releases, and APIs are still in flux and subject to change.
>
> Writing 3rd party IntelliJ Plugins in Compose for Desktop is currently **not officially supported** by the IntelliJ
> Platform. It should work, but your mileage may vary, and if things break you're on your own.
>
> Use at your own risk!
Jewel provides an implementation of the IntelliJ Platform themes that can be used in any Compose for Desktop
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]
> <a href="https://www.droidcon.com/2023/11/15/meet-jewelcreate-ide-plugins-in-compose/">
> <img src="https://i.vimeocdn.com/video/1749849437-f275e0337faca5cedab742ea157abbafe5a0207d3a59db891a72b6180ce13a6c-d?mh=120" align="left" />
> </a>
>
> 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.<br clear="left" />
<br/>
## Getting started
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`:
```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())
}
```
> [!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 {
// 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-[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:
```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")
}
}
```
<br/>
> [!TIP]
> It's easier to use version catalogs — you can use the Jewel [version catalog](gradle/libs.versions.toml) as reference.
<br/>
## 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
The project is split in modules:
1. `buildSrc` contains the build logic, including:
* The `jewel` and `jewel-publish` configuration plugins
* The `jewel-check-public-api` and `jewel-linting` configuration plugins
* The Theme Palette generator plugin
* The Studio Releases generator plugin
2. `foundation` contains the foundational Jewel functionality:
* Basic components without strong styling (e.g., `SelectableLazyColumn`, `BasicLazyTree`)
* The `JewelTheme` interface with a few basic composition locals
* The state management primitives
* The Jewel annotations
* A few other primitives
3. `ui` contains all the styled components and custom painters logic
4. `decorated-window` contains basic, unstyled functionality to have custom window decoration on the JetBrains Runtime
5. `int-ui` contains two modules:
* `int-ui-standalone` has a standalone version of the Int UI styling values that can be used in any Compose for
Desktop app
* `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. `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
## Branching strategy and IJ Platforms
Code on the main branch is developed and tested against the current latest IntelliJ Platform version.
When the EAP for a new major version starts, we cut a `releases/xxx` release branch, where `xxx` is the tracked major
IJP version. At that point, the main branch starts tracking the latest available major IJP version, and changes are
cherry-picked into each release branch as needed. All active release branches have the same functionality (where
supported by the corresponding IJP version), but might differ in platform version-specific fixes and internals.
The standalone Int UI theme will always work the same way as the latest major IJP version; release branches will not
include the `int-ui` module, which is always released from the main branch.
Releases of Jewel are always cut from a tag on the main branch; the HEAD of each `releases/xxx` branch is then tagged
as `[mainTag]-xxx`, and used to publish the artifacts for that major IJP version.
> [!IMPORTANT]
> 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.02023.3.2 might or might not work.
> [!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.
For an example on how to set up a standalone app, you can refer to
the [`standalone` sample](samples/standalone/build.gradle.kts).
> [!WARNING]
> Note that Jewel **requires** the JetBrains Runtime to work correctly. Some features like font loading depend on it,
> as it has extra features and patches for UI functionalities that aren't available in other JDKs.
> We **do not support** running Jewel on any other JDK.
To use Jewel components in a non-IntelliJ Platform environment, you need to wrap your UI hierarchy in a `IntUiTheme`
composable:
```kotlin
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
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](art/docs/custom-chrome.png)
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.
To get an IntelliJ-like custom title bar, you need to pass the window decoration styling to your theme call, and add the
`DecoratedWindow` composable at the top level of the theme:
```kotlin
IntUiTheme(
theme = themeDefinition,
styling = ComponentStyling.default().decoratedWindow(
titleBarStyle = TitleBarStyle.light()
),
) {
DecoratedWindow(
onCloseRequest = { exitApplication() },
) {
// ...
}
}
```
## 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.
This bridge ensures that we pick up the colours, typography, metrics, and images as defined in the current IntelliJ
theme, and apply them to the Compose components as well. This means Jewel will automatically adapt to IntelliJ Platform
themes that use the [standard theming](https://plugins.jetbrains.com/docs/intellij/themes-getting-started.html)
mechanisms.
> [!NOTE]
> IntelliJ themes that use non-standard mechanisms (such as providing custom UI implementations for Swing components)
> are not, and can never, be supported.
If you're writing an IntelliJ Platform plugin, you should use the `SwingBridgeTheme` instead of the standalone theme:
```kotlin
SwingBridgeTheme {
// ...
}
```
### 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.3 (EAP 6+) | `main` |
| 2024.2 (beta 1+) | `releases/242` |
| 2024.1 (EAP 3+) | `releases/241` |
| 2023.3 (**archived**) | `archived-releases/233` |
| 2023.2 (**archived**) | `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
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
Icon(key = MyIconKeys.myIcon, contentDescription = "My icon")
```
### 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
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")
}
```
> [!NOTE]
> If you are targeting an IntelliJ plugin, you don't need this additional setup since the icons are provided by the
> platform itself.
### Loading your own 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.
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
`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 indeterminateHint =
if (myState.toggleableState == ToggleableState.Indeterminate) {
IndeterminateHint
} else {
PainterHint.None
}
Icon(
key = myKey,
contentDescription = "My icon",
indeterminateHint,
Selected(myState),
Stateful(myState),
)
```
Where the `IndeterminateHint` looks like this:
```kotlin
private object IndeterminateHint : PainterSuffixHint() {
override fun suffix(): String = "Indeterminate"
}
```
Assuming the PainterProvider has a base path of `components/myIcon.svg`, Jewel will automatically translate it to the
right path based on the state. If you want to learn more about this system, look at the `PainterHint` interface and its
implementations.
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:
```kotlin
val myFamily = FontFamily("My Family")
```
If you want to use a font embedded in the JetBrains Runtime, you can use the `EmbeddedFontFamily` API instead:
```kotlin
import javax.swing.text.StyledEditorKit.FontFamilyAction
// Will return null if no matching font family exists in the JBR
val myEmbeddedFamily = EmbeddedFontFamily("Embedded family")
// It's recommended to load a fallback family when dealing with embedded familes
val myFamily = myEmbeddedFamily ?: FontFamily("Fallback family")
```
You can obtain a `FontFamily` from any `java.awt.Font` — including from `JBFont`s — by using the `asComposeFontFamily()`
API:
```kotlin
val myAwtFamily = myFont.asComposeFontFamily()
// This will attempt to resolve the logical AWT font
val myLogicalFamily = Font("Dialog").asComposeFontFamily()
// This only works in the IntelliJ Platform,
// since JBFont is only available there
val myLabelFamily = JBFont.label().asComposeFontFamily()
```
## 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
[experimental Swing rendering pipeline](https://blog.jetbrains.com/kotlin/2023/08/compose-multiplatform-1-5-0-release/#enhanced-swing-interop)
before you initialize Compose content.
The `ToolWindow.addComposeTab()` extension function provided by the `ide-laf-bridge` module will take care of that for
you. However, if you want to also enable it in other scenarios and in standalone applications, you can call the
`enableNewSwingCompositing()` function in your Compose entry points (that is, right before creating a `ComposePanel`).
> [!NOTE]
> The new Swing rendering pipeline is experimental and may have performance repercussions when using infinitely
> repeating animations. This is a known issue by the Compose Multiplatform team, that requires changes in the Java
> runtime to fix. Once the required changes are made in the JetBrains Runtime, we'll remove this notice.
## Written with Jewel
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!
## Throubleshooting
### Git push hook is not working?
On git push you see:
```bash
error: cannot spawn .git/hooks/pre-push: No such file or directory
error: waitpid for (NULL) failed: No child processes
```
Try running `git lfs update --force`.
## Need help?
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 20224 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.
```

View File

@@ -0,0 +1,40 @@
# Releasing new versions of Jewel
The release process is mostly automated, but it requires manual prep steps.
> [!IMPORTANT]
> Don't arbitrarily create releases. Always obtain written go-ahead from @rock3r before cutting a release!
1. Update the Jewel version in [gradle.properties]
2. Commit the change with a message like "Bump version to x.y.z"
3. Ensure that Jewel works correctly in all its parts:
* Run all tests and checks
* Run the standalone sample and check _everything_ works as expected (in all themes, with/without Swing compat, ...)
* Run the IDE sample and check _everything_ works as expected (in all themes)
4. _For every branch in `releases/`_:
a. Check out the branch
b. Merge the `main` branch in using the _Merge_ dialog and these options:
![The merge dialog showing the --ff option and a custom commit message for the merge commit](art/docs/merge-dialog.png)
c. If needed, solve merge conflicts, as it makes sense
> [!CAUTION]
> Merging is the most delicate part. Not all changes need backporting, and not all changes can be
> backported without adapting to how an older IJP version works. **Triple check all backports!**
d. Repeat step 3 for this branch
5. Once all branches are ready, run the `:tagRelease` task, which will validate that the merge was done properly, and
that the version has been bumped
6. Push all the branches and all the new tags
7. Open the [tags page](https://github.com/JetBrains/jewel/tags), select the _base_ tag (e.g., `v1.0.0`, and not any of
the `v1.0.0-*` sub-tags)
8. Create a release from the tag, using the auto-generated release notes. Add any further information that may be useful
to users, including any breaking API or behaviour changes
* You can use an LLM to help you with this step if you want. Use the [generate-release-patch.sh] script to generate
a
patch file with all the changes since the last tag (make sure to run it on main!). You can then use a service like
Gemini Advanced or ChatGPT to generate release notes from it.
* An example prompt that produces decent results with Gemini
Advanced: https://gist.github.com/rock3r/3ed8ec836143049da834505ce5315fce
* **ALWAYS double check** the LLM output. Don't trust it! Treat it only a starting point to help you, not the end
result.
9. Wait for the [publishing CI job](https://github.com/JetBrains/jewel/actions/workflows/publish.yml) to finish running
10. Done!

View File

@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
This code is provided as-is. We'll fix issues as quickly as possible, but no
warranty of any kind is provided that the project is problem-free. Please do
not use this software in security-critical situations without a proper
analysis and vetting of its sources first from a security standpoint — we
have not done it.
## Reporting a Vulnerability
If you find a vulnerability, please report it as an issue. We'll address it
as soon as we can.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,7 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="black"/>
<rect x="256" y="76" width="254.558" height="254.558" transform="rotate(45 256 76)" fill="white"/>
<path d="M256 76L436 256H256V76Z" fill="#CCCCCC"/>
<path d="M76 256L256 436V256H76Z" fill="#CCCCCC"/>
<path d="M256 436L436 256H256V436Z" fill="#808080"/>
</svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -0,0 +1,185 @@
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.composeDesktop) apply false
alias(libs.plugins.compose.compiler) apply false
`jewel-linting`
}
dependencies {
sarif(projects.decoratedWindow)
sarif(projects.foundation)
sarif(projects.ideLafBridge)
sarif(projects.intUi.intUiDecoratedWindow)
sarif(projects.intUi.intUiStandalone)
sarif(projects.markdown.core)
sarif(projects.markdown.extension.autolink)
sarif(projects.markdown.extension.gfmAlerts)
sarif(projects.markdown.ideLafBridgeStyling)
sarif(projects.markdown.intUiStandaloneStyling)
sarif(projects.samples.idePlugin)
sarif(projects.samples.standalone)
sarif(projects.ui)
}
// TODO remove this once the Skiko fix makes it into CMP 1.7.1
allprojects {
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "org.jetbrains.skiko") {
useVersion("0.8.17")
because("Contains important memory usage fix")
}
}
}
}
}
tasks {
// val mergeSarifReports by
// registering(MergeSarifTask::class) {
// source(configurations.outgoingSarif)
// include { it.file.extension == "sarif" }
// }
//
// register("check") { dependsOn(mergeSarifReports) }
register("tagRelease") {
description = "Tags main branch and releases branches with provided tag name"
group = "release"
doFirst {
val rawReleaseVersion =
((project.property("jewel.release.version") as String?)?.takeIf { it.isNotBlank() }
?: throw GradleException("Please provide a jewel.release.version in gradle.properties"))
val releaseName = "v$rawReleaseVersion"
val stdOut = ByteArrayOutputStream()
// Check we're on the main branch
logger.info("Checking current branch is main...")
exec {
commandLine = listOf("git", "rev-parse", "--abbrev-ref", "HEAD")
standardOutput = stdOut
}
.assertNormalExitValue()
val currentBranch = stdOut.use { it.toString() }.trim()
if (currentBranch != "main") {
throw GradleException("This task must only be run on the main branch")
}
// Check tag doesn't already exist
logger.info("Checking current branch is main...")
stdOut.reset()
exec {
commandLine = listOf("git", "tag")
standardOutput = stdOut
}
.assertNormalExitValue()
if (stdOut.toString().trim().lines().any { it == releaseName }) {
throw GradleException("The tag $releaseName already exists!")
}
// Check there are no uncommitted changes
logger.info("Checking all changes have been committed...")
stdOut.reset()
exec {
commandLine = listOf("git", "status", "--porcelain")
standardOutput = stdOut
}
.assertNormalExitValue()
if (stdOut.toString().isNotBlank()) {
throw GradleException("Please commit all changes before tagging a release")
}
// Get the current HEAD hash
logger.info("Getting HEAD hash...")
stdOut.reset()
exec {
commandLine = listOf("git", "rev-parse", "HEAD")
standardOutput = stdOut
}
.assertNormalExitValue()
val currentHead = stdOut.use { it.toString() }.trim()
// Enumerate the release branches
logger.info("Enumerating release branches...")
stdOut.reset()
exec {
commandLine = listOf("git", "branch")
standardOutput = stdOut
}
.assertNormalExitValue()
val releaseBranches =
stdOut
.use { it.toString() }
.lines()
.filter { it.trim().startsWith("releases/") }
.map { it.trim().removePrefix("releases/") }
if (releaseBranches.isEmpty()) {
throw GradleException("No local release branches found, make sure they exist locally")
}
logger.lifecycle("Release branches: ${releaseBranches.joinToString { "releases/$it" }}")
// Check all release branches have gotten the latest from main
logger.info("Validating release branches...")
for (branch in releaseBranches) {
stdOut.reset()
exec {
commandLine = listOf("git", "merge-base", "main", "releases/$branch")
standardOutput = stdOut
}
.assertNormalExitValue()
val mergeBase = stdOut.use { it.toString() }.trim()
if (mergeBase != currentHead) {
throw GradleException("Branch releases/$branch is not up-to-date with main!")
}
}
// Tag main branch
logger.lifecycle("Tagging head of main branch as $releaseName...")
exec { commandLine = listOf("git", "tag", releaseName) }.assertNormalExitValue()
// Tag release branches
for (branch in releaseBranches) {
val branchTagName = "$releaseName-$branch"
logger.lifecycle("Tagging head of branch releases/$branch as $branchTagName...")
stdOut.reset()
logger.info("Getting branch head commit...")
exec {
commandLine = listOf("git", "rev-parse", "releases/$branch")
standardOutput = stdOut
}
.assertNormalExitValue()
val branchHead = stdOut.use { it.toString() }.trim()
logger.info("HEAD of releases/$branch is $branchHead")
logger.info("Tagging commit ${branchHead.take(7)} as $branchTagName")
stdOut.reset()
exec {
commandLine = listOf("git", "tag", branchTagName, branchHead)
standardOutput = stdOut
}
.assertNormalExitValue()
}
logger.info("All done!")
}
}
register<Delete>("cleanTestPublishArtifacts") { delete(rootProject.layout.buildDirectory.dir("maven-test")) }
register<Delete>("clean") { delete(rootProject.layout.buildDirectory) }
}

View File

@@ -0,0 +1,36 @@
import java.util.Properties
plugins {
`kotlin-dsl`
alias(libs.plugins.kotlinx.serialization)
}
val properties = Properties()
project.file("../gradle.properties").inputStream().use { properties.load(it) }
val jdkLevel = properties.getProperty("jdk.level") as String
kotlin {
jvmToolchain { languageVersion = JavaLanguageVersion.of(jdkLevel) }
sourceSets { all { languageSettings { optIn("kotlinx.serialization.ExperimentalSerializationApi") } } }
}
dependencies {
implementation(libs.detekt.gradlePlugin)
implementation(libs.dokka.gradlePlugin)
implementation(libs.kotlin.gradlePlugin)
implementation(libs.kotlinSarif)
implementation(libs.kotlinpoet)
implementation(libs.kotlinter.gradlePlugin)
implementation(libs.ktfmt.gradlePlugin)
implementation(libs.kotlinx.binaryCompatValidator.gradlePlugin)
implementation(libs.kotlinx.serialization.json)
implementation(libs.poko.gradlePlugin)
// Enables using type-safe accessors to reference plugins from the [plugins] block defined in
// version catalogs.
// Context: https://github.com/gradle/gradle/issues/15383#issuecomment-779893192
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}

View File

@@ -0,0 +1,19 @@
@file:Suppress("UnstableApiUsage")
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "buildSrc"
dependencyResolutionManagement {
repositories {
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://www.jetbrains.com/intellij-repository/releases")
maven("https://www.jetbrains.com/intellij-repository/snapshots")
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
gradlePluginPortal()
mavenCentral()
}
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
}

View File

@@ -0,0 +1,63 @@
import io.github.detekt.sarif4k.Run
import io.github.detekt.sarif4k.SarifSchema210
import io.github.detekt.sarif4k.Tool
import io.github.detekt.sarif4k.ToolComponent
import io.github.detekt.sarif4k.Version
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
private const val SARIF_SCHEMA =
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
@CacheableTask
open class MergeSarifTask : SourceTask() {
init {
group = "verification"
}
@get:OutputFile
val mergedSarifPath: RegularFileProperty =
project.objects.fileProperty().convention(project.layout.buildDirectory.file("reports/static-analysis.sarif"))
@TaskAction
fun merge() {
val json = Json { prettyPrint = true }
logger.lifecycle("Merging ${source.files.size} SARIF file(s)...")
logger.lifecycle(source.files.joinToString("\n") { " * ~${it.path.removePrefix(project.rootDir.path)}" })
val merged =
SarifSchema210(
schema = SARIF_SCHEMA,
version = Version.The210,
runs =
source.files
.asSequence()
.filter { it.extension == "sarif" }
.map { file -> file.inputStream().use { json.decodeFromStream<SarifSchema210>(it) } }
.flatMap { report -> report.runs }
.groupBy { run -> run.tool.driver.guid ?: run.tool.driver.name }
.values
.asSequence()
.filter { it.isNotEmpty() }
.map { runs ->
Run(
results = runs.flatMap { it.results.orEmpty() },
tool = Tool(driver = ToolComponent(name = "Jewel static analysis")),
)
}
.toList(),
)
logger.lifecycle("Merged SARIF file contains ${merged.runs.size} run(s)")
logger.info("Writing merged SARIF file to $mergedSarifPath...")
mergedSarifPath.asFile.get().outputStream().use { json.encodeToStream(merged, it) }
}
}

View File

@@ -0,0 +1,38 @@
@file:Suppress("UnstableApiUsage", "UnusedImports")
import org.gradle.api.Project
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(project: Project) {
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")
}
}
maven(project.rootProject.layout.buildDirectory.dir("maven-test")) { name = "LocalTest" }
}
}
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"
}
}

View File

@@ -0,0 +1,128 @@
import java.io.File
import java.util.Stack
import java.util.regex.PatternSyntaxException
import org.gradle.api.GradleException
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
@CacheableTask
open class ValidatePublicApiTask : SourceTask() {
@Input var excludedClassRegexes: Set<String> = emptySet()
init {
group = "verification"
// The output is never really used, it is here for cacheability reasons only
outputs.file(project.layout.buildDirectory.file("apiValidationRun"))
}
private val classFqnRegex = "public (?:\\w+ )*class (\\S+)\\b".toRegex()
@Suppress("ConvertToStringTemplate") // The odd concatenation is needed because of $; escapes get confused
private val copyMethodRegex = ("public static synthetic fun copy(-\\w+)?" + "\\$" + "default\\b").toRegex()
@TaskAction
fun validatePublicApi() {
logger.info("Validating ${source.files.size} API file(s)...")
val violations = mutableMapOf<File, Set<String>>()
val excludedRegexes =
excludedClassRegexes
.map {
try {
it.toRegex()
} catch (ignored: PatternSyntaxException) {
throw GradleException("Invalid data exclusion regex: '$it'")
}
}
.toSet()
inputs.files.forEach { apiFile ->
logger.lifecycle("Validating public API from file ${apiFile.path}")
apiFile.useLines { lines ->
val actualDataClasses = findDataClasses(lines).filterExclusions(excludedRegexes)
if (actualDataClasses.isNotEmpty()) {
violations[apiFile] = actualDataClasses
}
}
}
if (violations.isNotEmpty()) {
val message = buildString {
appendLine("Data classes found in public API.")
appendLine()
for ((file, dataClasses) in violations.entries) {
appendLine("In file ${file.path}:")
for (dataClass in dataClasses) {
appendLine(" * ${dataClass.replace("/", ".")}")
}
appendLine()
}
}
throw GradleException(message)
} else {
logger.lifecycle("No public API violations found.")
}
}
private fun findDataClasses(lines: Sequence<String>): Set<String> {
val currentClassStack = Stack<String>()
val dataClasses = mutableMapOf<String, DataClassInfo>()
for (line in lines) {
if (line.isBlank()) continue
val matchResult = classFqnRegex.find(line)
if (matchResult != null) {
val classFqn = matchResult.groupValues[1]
currentClassStack.push(classFqn)
continue
}
if (line.contains("}")) {
currentClassStack.pop()
continue
}
val fqn = currentClassStack.peek()
if (copyMethodRegex.find(line) != null) {
val info = dataClasses.getOrPut(fqn) { DataClassInfo(fqn) }
info.hasCopyMethod = true
} else if (line.contains("public static final synthetic fun box-impl")) {
val info = dataClasses.getOrPut(fqn) { DataClassInfo(fqn) }
info.isLikelyValueClass = true
}
}
val actualDataClasses = dataClasses.filterValues { it.hasCopyMethod && !it.isLikelyValueClass }.keys
return actualDataClasses
}
private fun Set<String>.filterExclusions(excludedRegexes: Set<Regex>): Set<String> {
if (excludedRegexes.isEmpty()) return this
return filterNot { dataClassFqn ->
val isExcluded = excludedRegexes.any { it.matchEntire(dataClassFqn) != null }
if (isExcluded) {
logger.info(" Ignoring excluded data class $dataClassFqn")
}
isExcluded
}
.toSet()
}
}
@Suppress("DataClassShouldBeImmutable") // Only used in a loop, saves memory and is faster
private data class DataClassInfo(
val fqn: String,
var hasCopyMethod: Boolean = false,
var isLikelyValueClass: Boolean = false,
)

View File

@@ -0,0 +1,17 @@
import com.squareup.kotlinpoet.ClassName
import org.jetbrains.jewel.buildlogic.demodata.AndroidStudioReleasesGeneratorTask
import org.jetbrains.jewel.buildlogic.demodata.STUDIO_RELEASES_OUTPUT_CLASS_NAME
import org.jetbrains.jewel.buildlogic.demodata.StudioVersionsGenerationExtension
val extension: StudioVersionsGenerationExtension =
extensions.findByType<StudioVersionsGenerationExtension>()
?: extensions.create("androidStudioReleasesGenerator", StudioVersionsGenerationExtension::class.java)
val task =
tasks.register<AndroidStudioReleasesGeneratorTask>("generateAndroidStudioReleasesList") {
val className = ClassName.bestGuess(STUDIO_RELEASES_OUTPUT_CLASS_NAME)
val filePath = className.packageName.replace(".", "/") + "/${className.simpleName}.kt"
outputFile = extension.targetDir.file(filePath)
dataUrl = extension.dataUrl
resourcesDirs = extension.resourcesDirs
}

View File

@@ -0,0 +1,251 @@
@file:Suppress("UnstableApiUsage")
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
import java.lang.reflect.Field
import java.net.URLClassLoader
private val defaultOutputDir: Provider<Directory> = layout.buildDirectory.dir("generated/iconKeys")
class IconKeysGeneratorContainer(container: NamedDomainObjectContainer<IconKeysGeneration>) :
NamedDomainObjectContainer<IconKeysGeneration> by container
class IconKeysGeneration(val name: String, project: Project) {
val outputDirectory: DirectoryProperty = project.objects.directoryProperty().convention(defaultOutputDir)
val sourceClassName: Property<String> = project.objects.property<String>()
val generatedClassName: Property<String> = project.objects.property<String>()
}
val iconGeneration by
configurations.registering {
isCanBeConsumed = false
isCanBeResolved = true
}
val extension = IconKeysGeneratorContainer(container<IconKeysGeneration> { IconKeysGeneration(it, project) })
extensions.add("intelliJIconKeysGenerator", extension)
extension.all item@{
val task =
tasks.register<IconKeysGeneratorTask>("generate${name}Keys") task@{
this@task.outputDirectory = this@item.outputDirectory
this@task.sourceClassName = this@item.sourceClassName
this@task.generatedClassName = this@item.generatedClassName
configuration.from(iconGeneration)
dependsOn(iconGeneration)
}
tasks {
withType<BaseKotlinCompile> { dependsOn(task) }
withType<Detekt> { dependsOn(task) }
withType<DokkaTask> { dependsOn(task) }
withType<Jar> { dependsOn(task) }
}
}
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
the<KotlinJvmProjectExtension>().sourceSets["main"].kotlin.srcDir(defaultOutputDir)
}
open class IconKeysGeneratorTask : DefaultTask() {
@get:OutputDirectory val outputDirectory: DirectoryProperty = project.objects.directoryProperty()
@get:Input val sourceClassName = project.objects.property<String>()
@get:Input val generatedClassName = project.objects.property<String>()
@get:InputFiles val configuration: ConfigurableFileCollection = project.objects.fileCollection()
init {
group = "jewel"
}
@TaskAction
fun generate() {
val guessedSourceClassName =
sourceClassName.map { ClassName.bestGuess(it).canonicalName.replace('.', '/') + ".kt" }.get()
// The icons artifacts are loaded on the iconGeneration configuration's classpath,
// so we need a classloader that can access these classes.
val classLoader = createClassLoader()
val sourceClass =
classLoader.loadClass(sourceClassName.get())
?: throw GradleException(
"Unable to load ${sourceClassName.get()}. " +
"Is the correct dependency declared on the iconGeneration configuration?"
)
// Traverse sourceClass by using reflection, collecting all members
// This step uses the mappings to add the new paths where available.
val dummyIconClass = classLoader.loadClass("com.intellij.ui.DummyIconImpl")
val oldUiPathField = dummyIconClass.getOriginalPathField()
val newUiPathField = dummyIconClass.getNewUiPathField()
val rootHolder = IconKeyHolder(sourceClass.simpleName)
whileForcingAccessible(oldUiPathField, newUiPathField) {
visitSourceClass(sourceClass, rootHolder, oldUiPathField, newUiPathField, classLoader)
}
logger.lifecycle("Read icon keys from ${sourceClass.name}")
// Step 4) Generate output Kotlin file
val fileSpec = generateKotlinCode(rootHolder)
val directory = outputDirectory.get().asFile
fileSpec.writeTo(directory)
logger.lifecycle("Written icon keys for $guessedSourceClassName into $directory")
}
private fun createClassLoader(): URLClassLoader {
val arrayOfURLs = configuration.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(arrayOfURLs, IconKeysGeneratorTask::class.java.classLoader)
}
private fun whileForcingAccessible(vararg fields: Field, action: () -> Unit) {
val wasAccessibles = mutableListOf<Boolean>()
for (field in fields) {
@Suppress("DEPRECATION")
wasAccessibles += field.isAccessible
field.isAccessible = true
}
try {
action()
} finally {
for ((index, field) in fields.withIndex()) {
field.isAccessible = wasAccessibles[index]
}
}
}
private fun visitSourceClass(
sourceClass: Class<*>,
parentHolder: IconKeyHolder,
oldUiPathField: Field,
newUiPathField: Field,
classLoader: ClassLoader,
) {
for (child in sourceClass.declaredClasses) {
val childName = "${parentHolder.name}.${child.simpleName}"
val childHolder = IconKeyHolder(childName)
parentHolder.holders += childHolder
visitSourceClass(child, childHolder, oldUiPathField, newUiPathField, classLoader)
}
parentHolder.holders.sortBy { it.name }
sourceClass.declaredFields
.filter { it.type == javax.swing.Icon::class.java }
.forEach { field ->
val fieldName = "${parentHolder.name}.${field.name}"
if (field.annotations.any { it.annotationClass == java.lang.Deprecated::class }) {
logger.lifecycle("Ignoring deprecated field: $fieldName")
return@forEach
}
val icon = field.get(sourceClass)
val oldUiPath =
oldUiPathField.get(icon) as String? ?: throw GradleException("Found null path in icon $fieldName")
validatePath(oldUiPath, fieldName, classLoader)
// New UI paths may be "partial", meaning they end with a / character.
// In this case, we're supposed to append the old UI path to the new UI
// path, because that's just how they decided to encode things in IJP.
val newUiPath =
(newUiPathField.get(icon) as String?)?.let { if (it.endsWith("/")) it + oldUiPath else it }
?: oldUiPath
validatePath(newUiPath, fieldName, classLoader)
parentHolder.keys += IconKey(fieldName, oldUiPath, newUiPath)
}
parentHolder.keys.sortBy { it.name }
}
private fun validatePath(path: String, fieldName: String, classLoader: ClassLoader) {
val iconsClass = classLoader.loadClass(sourceClassName.get())
if (iconsClass.getResourceAsStream("/${path.trimStart('/')}") == null) {
logger.warn("Icon $fieldName: '$path' does not exist")
}
}
private fun generateKotlinCode(rootHolder: IconKeyHolder): FileSpec {
val className = ClassName.bestGuess(generatedClassName.get())
return FileSpec.builder(className)
.apply {
indent(" ")
addFileComment("Generated by the Jewel icon keys generator\n")
addFileComment("Source class: ${sourceClassName.get()}")
addImport(keyClassName.packageName, keyClassName.simpleName)
val objectName = ClassName.bestGuess(generatedClassName.get())
addType(
TypeSpec.objectBuilder(objectName)
.apply {
for (childHolder in rootHolder.holders) {
generateKotlinCodeInner(childHolder, className)
}
for (key in rootHolder.keys) {
addProperty(buildIconKeyEntry(key, className))
}
}
.build()
)
}
.build()
}
private fun TypeSpec.Builder.generateKotlinCodeInner(holder: IconKeyHolder, rootClassName: ClassName) {
val objectName = holder.name.substringAfterLast('.')
addType(
TypeSpec.objectBuilder(objectName)
.apply {
for (childHolder in holder.holders) {
generateKotlinCodeInner(childHolder, rootClassName)
}
for (key in holder.keys) {
addProperty(buildIconKeyEntry(key, rootClassName))
}
}
.build()
)
}
private fun buildIconKeyEntry(key: IconKey, rootClassName: ClassName) =
PropertySpec.builder(key.name.substringAfterLast('.'), keyClassName)
.initializer(
"%L",
"IntelliJIconKey(\"${key.oldPath}\", " +
"\"${key.newPath ?: key.oldPath}\", " +
"${rootClassName.simpleName}::class.java)",
)
.build()
companion object {
private fun Class<*>.getOriginalPathField(): Field = declaredFields.first { it.name == "originalPath" }
private fun Class<*>.getNewUiPathField(): Field = declaredFields.first { it.name == "expUIPath" }
private val keyClassName = ClassName("org.jetbrains.jewel.ui.icon", "IntelliJIconKey")
}
}
private data class IconKeyHolder(
val name: String,
val holders: MutableList<IconKeyHolder> = mutableListOf(),
val keys: MutableList<IconKey> = mutableListOf(),
)
private data class IconKey(val name: String, val oldPath: String, val newPath: String?)

View File

@@ -0,0 +1,3 @@
import org.jetbrains.jewel.buildlogic.ideversion.CheckIdeaVersionTask
tasks.register<CheckIdeaVersionTask>("checkLatestIntelliJPlatformBuild")

View File

@@ -0,0 +1,42 @@
@file:Suppress("UnstableApiUsage")
import com.squareup.kotlinpoet.ClassName
import io.gitlab.arturbosch.detekt.Detekt
import org.gradle.util.internal.GUtil
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.jewel.buildlogic.theme.IntelliJThemeGeneratorTask
import org.jetbrains.jewel.buildlogic.theme.ThemeGeneration
import org.jetbrains.jewel.buildlogic.theme.ThemeGeneratorContainer
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
val extension = ThemeGeneratorContainer(container<ThemeGeneration> { ThemeGeneration(it, project) })
extensions.add("intelliJThemeGenerator", extension)
extension.all {
val task =
tasks.register<IntelliJThemeGeneratorTask>("generate${GUtil.toCamelCase(name)}Theme") {
val paths =
this@all.themeClassName.map {
val className = ClassName.bestGuess(it)
className.packageName.replace(".", "/") + "/${className.simpleName}.kt"
}
outputFile = targetDir.file(paths)
themeClassName = this@all.themeClassName
ideaVersion = this@all.ideaVersion
themeFile = this@all.themeFile
}
tasks {
withType<BaseKotlinCompile> { dependsOn(task) }
withType<Detekt> { dependsOn(task) }
withType<DokkaTask> { dependsOn(task) }
withType<Jar> { dependsOn(task) }
}
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
the<KotlinJvmProjectExtension>().sourceSets["main"].kotlin.srcDir(targetDir)
}
}

View File

@@ -0,0 +1,37 @@
@file:Suppress("UnstableApiUsage")
import org.jetbrains.jewel.buildlogic.apivalidation.ApiValidationExtension
plugins {
id("org.jetbrains.kotlinx.binary-compatibility-validator")
id("dev.drewhamilton.poko")
kotlin("jvm")
}
apiValidation {
/**
* Set of annotations that exclude API from being public. Typically, it is all kinds of `@InternalApi` annotations
* that mark effectively private API that cannot be actually private for technical reasons.
*/
nonPublicMarkers.add("org.jetbrains.jewel.InternalJewelApi")
}
poko { pokoAnnotation = "org/jetbrains/jewel/foundation/GenerateDataFunctions" }
kotlin { explicitApi() }
val extension = project.extensions.create("publicApiValidation", ApiValidationExtension::class.java)
with(extension) { excludedClassRegexes.convention(emptySet()) }
tasks {
val validatePublicApi =
register<ValidatePublicApiTask>("validatePublicApi") {
include { it.file.extension == "api" }
source(project.fileTree("api"))
dependsOn(named("apiCheck"))
excludedClassRegexes = project.the<ApiValidationExtension>().excludedClassRegexes.get()
}
named("check") { dependsOn(validatePublicApi) }
}

View File

@@ -0,0 +1,25 @@
@file:Suppress("UnstableApiUsage")
plugins {
id("io.gitlab.arturbosch.detekt")
id("org.jmailen.kotlinter")
id("com.ncorti.ktfmt.gradle")
}
configurations {
val dependencies = register("sarif") { isCanBeDeclared = true }
register("outgoingSarif") {
isCanBeConsumed = true
isCanBeResolved = true
extendsFrom(dependencies.get())
attributes { attribute(Usage.USAGE_ATTRIBUTE, objects.named("sarif")) }
}
}
ktfmt {
maxWidth = 120
blockIndent = 4
continuationIndent = 4
manageTrailingCommas = true
removeUnusedImports = true
}

View File

@@ -0,0 +1,56 @@
@file:Suppress("UnstableApiUsage")
plugins {
kotlin("jvm")
`maven-publish`
id("org.jetbrains.dokka")
signing
}
val sourcesJar by
tasks.registering(Jar::class) {
from(kotlin.sourceSets.main.map { it.kotlin })
archiveClassifier = "sources"
destinationDirectory = layout.buildDirectory.dir("artifacts")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val javadocJar by
tasks.registering(Jar::class) {
from(tasks.dokkaHtml)
archiveClassifier = "javadoc"
destinationDirectory = layout.buildDirectory.dir("artifacts")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val publishingExtension = extensions.getByType<PublishingExtension>()
signing {
useInMemoryPgpKeys(
System.getenv("PGP_PRIVATE_KEY") ?: properties["signing.privateKey"] as String?,
System.getenv("PGP_PASSWORD") ?: properties["signing.password"] as String?,
)
if (project.hasProperty("no-sign")) {
logger.warn("⚠️ CAUTION! NO-SIGN MODE ENABLED, PUBLICATIONS WON'T BE SIGNED")
} else {
sign(publishingExtension.publications)
}
}
publishing {
configureJewelRepositories(project)
val ijpTarget = project.property("ijp.target") as String
publications {
register<MavenPublication>("main") {
from(components["kotlin"])
artifact(javadocJar)
artifact(sourcesJar)
version = project.version as String
artifactId = "jewel-${project.name}-$ijpTarget"
pom { configureJewelPom() }
}
}
}

View File

@@ -0,0 +1,98 @@
import com.ncorti.ktfmt.gradle.tasks.KtfmtBaseTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("jewel-linting")
kotlin("jvm")
}
group = "org.jetbrains.jewel"
val gitHubRef: String? = System.getenv("GITHUB_REF")
version =
when {
properties.containsKey("versionOverride") -> {
val rawVersion = (properties["versionOverride"] as String).trim()
if (!rawVersion.matches("^\\d\\.\\d{2,}\\.\\d+$".toRegex())) {
throw GradleException("Invalid versionOverride: $rawVersion")
}
logger.warn("Using version override: $rawVersion")
rawVersion
}
gitHubRef?.startsWith("refs/tags/") == true -> {
gitHubRef.substringAfter("refs/tags/").removePrefix("v")
}
properties.containsKey("useCurrentVersion") -> {
val rawVersion = (properties["jewel.release.version"] as String).trim()
if (!rawVersion.matches("^\\d\\.\\d{2,}\\.\\d+$".toRegex())) {
throw GradleException("Invalid jewel.release.version found in gradle.properties: $rawVersion")
}
logger.warn("Using jewel.release.version: $rawVersion")
rawVersion
}
else -> "1.0.0-SNAPSHOT"
}
val jdkLevel = project.property("jdk.level") as String
kotlin {
jvmToolchain { languageVersion = JavaLanguageVersion.of(jdkLevel) }
compilerOptions.jvmTarget.set(JvmTarget.fromTarget(jdkLevel))
target {
compilations.all { kotlinOptions { freeCompilerArgs += "-Xcontext-receivers" } }
sourceSets.all {
languageSettings {
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
optIn("androidx.compose.ui.ExperimentalComposeUiApi")
optIn("kotlin.experimental.ExperimentalTypeInference")
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
optIn("org.jetbrains.jewel.foundation.ExperimentalJewelApi")
optIn("org.jetbrains.jewel.foundation.InternalJewelApi")
}
}
}
}
detekt {
config.from(files(rootProject.file("detekt.yml")))
buildUponDefaultConfig = true
}
val sarifReport: Provider<RegularFile> = layout.buildDirectory.file("reports/ktlint-${project.name}.sarif")
tasks {
detektMain {
val sarifOutputFile = layout.buildDirectory.file("reports/detekt-${project.name}.sarif")
exclude { it.file.absolutePath.startsWith(layout.buildDirectory.asFile.get().absolutePath) }
reports {
sarif.required = true
sarif.outputLocation = sarifOutputFile
}
}
formatKotlinMain { exclude { it.file.absolutePath.replace('\\', '/').contains("build/generated") } }
withType<KtfmtBaseTask> { exclude { it.file.absolutePath.contains("build/generated") } }
lintKotlinMain {
exclude { it.file.absolutePath.replace('\\', '/').contains("build/generated") }
reports = provider {
mapOf(
"plain" to layout.buildDirectory.file("reports/ktlint-${project.name}.txt").get().asFile,
"html" to layout.buildDirectory.file("reports/ktlint-${project.name}.html").get().asFile,
"sarif" to sarifReport.get().asFile,
)
}
}
}
configurations.named("sarif") {
outgoing {
artifact(tasks.detektMain.flatMap { it.sarifReportFile }) { builtBy(tasks.detektMain) }
artifact(sarifReport) { builtBy(tasks.lintKotlinMain) }
}
}

View File

@@ -0,0 +1,8 @@
package org.jetbrains.jewel.buildlogic.apivalidation
import org.gradle.api.provider.SetProperty
interface ApiValidationExtension {
val excludedClassRegexes: SetProperty<String>
}

View File

@@ -0,0 +1,87 @@
package org.jetbrains.jewel.buildlogic.demodata
import com.squareup.kotlinpoet.ClassName
import gradle.kotlin.dsl.accessors._327d2b3378ed6d2c1bec5d20438f90c7.kotlin
import gradle.kotlin.dsl.accessors._327d2b3378ed6d2c1bec5d20438f90c7.sourceSets
import java.io.File
import java.net.URI
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.gradle.api.DefaultTask
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.SourceSetContainer
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.setProperty
open class StudioVersionsGenerationExtension(project: Project) {
val targetDir: DirectoryProperty =
project.objects
.directoryProperty()
.convention(
project.layout.dir(project.provider { project.sourceSets.named("main").get().kotlin.srcDirs.first() })
)
val resourcesDirs: SetProperty<File> =
project.objects
.setProperty<File>()
.convention(
project.provider {
when {
project.plugins.hasPlugin("org.gradle.jvm-ecosystem") ->
project.extensions.getByType<SourceSetContainer>()["main"].resources.srcDirs
else -> emptySet()
}
}
)
val dataUrl: Property<String> =
project.objects.property<String>().convention("https://jb.gg/android-studio-releases-list.json")
}
internal const val STUDIO_RELEASES_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<String>()
@get:Input val resourcesDirs = project.objects.setProperty<File>()
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 = URI.create(url).toURL().openStream().use { json.decodeFromStream<ApiAndroidStudioReleases>(it) }
val className = ClassName.bestGuess(STUDIO_RELEASES_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}")
}
}

View File

@@ -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<File>) =
FileSpec.builder(className)
.apply {
indent(" ")
addFileComment("Generated by the Jewel Android Studio Releases Generator\n")
addFileComment("Generated from $url on ${ZonedDateTime.now()}")
addImport("org.jetbrains.jewel.samples.ideplugin.releasessample", "ContentItem.AndroidStudio")
addImport("kotlinx.datetime", "LocalDate")
addType(createBaseTypeSpec(className, releases, resourceDirs))
}
.build()
private fun createBaseTypeSpec(className: ClassName, releases: ApiAndroidStudioReleases, resourceDirs: Set<File>) =
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()
private fun readReleases(releases: ApiAndroidStudioReleases, resourceDirs: Set<File>) =
releases.content.item
.map { readRelease(it, resourceDirs) }
.joinToCode(prefix = "listOf(\n", separator = ",\n", suffix = ")")
private fun readRelease(release: ApiAndroidStudioReleases.Content.Item, resourceDirs: Set<File>) =
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<File>): 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"
}
}

View File

@@ -0,0 +1,35 @@
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<Item> = 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<Download> = 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,
)
}
}
}

View File

@@ -0,0 +1,19 @@
package org.jetbrains.jewel.buildlogic.ideversion
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal data class ApiIdeaReleasesItem(
@SerialName("code") val code: String,
@SerialName("releases") val releases: List<Release>,
) {
@Serializable
internal data class Release(
@SerialName("build") val build: String,
@SerialName("type") val type: String,
@SerialName("version") val version: String,
@SerialName("majorVersion") val majorVersion: String,
)
}

View File

@@ -0,0 +1,186 @@
package org.jetbrains.jewel.buildlogic.ideversion
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
open class CheckIdeaVersionTask : DefaultTask() {
private val releasesUrl =
"https://data.services.jetbrains.com/products?" +
"fields=code,releases,releases.version,releases.build,releases.type,releases.majorVersion&" +
"code=IC"
private val ideaVersionRegex = "202\\d\\.(\\d\\.)?\\d".toRegex(RegexOption.IGNORE_CASE)
private val intelliJPlatformBuildRegex = "2\\d{2}\\.\\d+\\.\\d+(?:-EAP-SNAPSHOT)?".toRegex(RegexOption.IGNORE_CASE)
private val currentIjpVersion = project.currentIjpVersion
init {
group = "jewel"
}
@TaskAction
fun generate() {
logger.lifecycle("Fetching IntelliJ Platform releases from $releasesUrl...")
val ideaVersion = readCurrentVersionInfo()
validateIdeaVersion(ideaVersion)
val platformBuildsForThisMajorVersion =
IJPVersionsFetcher.fetchBuildsForCurrentMajorVersion(releasesUrl, ideaVersion.majorVersion, logger)
if (platformBuildsForThisMajorVersion == null) {
logger.error("Cannot check platform version, no builds found for current version $ideaVersion")
return
}
val latestAvailableBuild = platformBuildsForThisMajorVersion.last()
logger.info("The latest IntelliJ Platform ${ideaVersion.version} build is ${latestAvailableBuild.build}")
val isCurrentBuildStable = ideaVersion.type.lowercase() != "eap"
if (IJPVersionsFetcher.compare(ideaVersion, latestAvailableBuild) < 0) {
throw GradleException(
buildString {
appendLine("IntelliJ Platform version dependency is out of date.")
appendLine()
append("Current build: ${ideaVersion.build}")
if (!isCurrentBuildStable) append("-EAP-SNAPSHOT")
appendLine()
appendLine("Current version: ${ideaVersion.version}")
append("Detected channel: ")
appendLine(latestAvailableBuild.type)
appendLine()
append("Latest build: ${latestAvailableBuild.build}")
if (!isCurrentBuildStable) append("-EAP-SNAPSHOT")
appendLine()
append("Latest version: ")
if (isCurrentBuildStable) {
appendLine(latestAvailableBuild.version)
} else {
appendLine(latestAvailableBuild.build.removeSuffix("-EAP-SNAPSHOT"))
}
appendLine(
"Please update the 'idea' and 'intelliJPlatformBuild' " + "versions in the catalog accordingly."
)
}
)
}
logger.lifecycle(
"No IntelliJ Platform version updates available. " +
"Current: ${ideaVersion.build} (${ideaVersion.version})"
)
}
private fun readCurrentVersionInfo(): ApiIdeaReleasesItem.Release {
val catalogFile = project.rootProject.file("gradle/libs.versions.toml")
val ideaVersion = readIdeaVersion(catalogFile)
val isStableBuild = !ideaVersion.matches(intelliJPlatformBuildRegex)
val platformBuild = readPlatformBuild(catalogFile)
val majorVersion =
if (isStableBuild) {
asMajorPlatformVersion(ideaVersion)
} else {
inferMajorPlatformVersion(platformBuild)
}
return ApiIdeaReleasesItem.Release(
build = platformBuild.removeSuffix("-EAP-SNAPSHOT"),
version = ideaVersion,
majorVersion = majorVersion,
type = if (isStableBuild) "release" else "eap",
)
}
private fun asMajorPlatformVersion(rawVersion: String) = rawVersion.take(6)
private fun inferMajorPlatformVersion(rawBuildNumber: String) =
"20${rawBuildNumber.take(2)}.${rawBuildNumber.substringBefore('.').last()}"
private fun readIdeaVersion(catalogFile: File): String {
val versionName = "idea"
val catalogDependencyLine =
catalogFile.useLines { lines -> lines.find { it.startsWith(versionName) } }
?: throw GradleException(
"Unable to find IJP dependency '$versionName' in the catalog file '${catalogFile.path}'"
)
val dependencyVersion =
catalogDependencyLine.substringAfter(versionName).trimStart(' ', '=').trimEnd().trim('"')
if (!dependencyVersion.matches(ideaVersionRegex) && !dependencyVersion.matches(intelliJPlatformBuildRegex)) {
throw GradleException("Invalid IJ IDEA version found in version catalog: '$dependencyVersion'")
}
return dependencyVersion
}
private fun readPlatformBuild(catalogFile: File): String {
val versionName = "intelliJPlatformBuild"
val catalogDependencyLine =
catalogFile.useLines { lines -> lines.find { it.startsWith(versionName) } }
?: throw GradleException(
"Unable to find IJP dependency '$versionName' in the catalog file '${catalogFile.path}'"
)
val declaredPlatformBuild =
catalogDependencyLine.substringAfter(versionName).trimStart(' ', '=').trimEnd().trim('"')
if (!declaredPlatformBuild.matches(intelliJPlatformBuildRegex)) {
throw GradleException("Invalid IJP build found in version catalog: '$declaredPlatformBuild'")
}
return declaredPlatformBuild
}
private fun validateIdeaVersion(currentVersion: ApiIdeaReleasesItem.Release) {
val candidateMatches =
IJPVersionsFetcher.fetchIJPVersions(releasesUrl, logger)
?: throw GradleException("Can't fetch all IJP releases.")
val match =
candidateMatches.find { it.build == currentVersion.build }
?: throw GradleException("IJ build ${currentVersion.build} seemingly does not exist")
if (currentVersion.type != "eap" && match.version != currentVersion.version) {
throw GradleException(
buildString {
appendLine("The 'idea' and 'intelliJPlatformBuild' properties in the catalog don't match.")
append("'idea' = ")
append(currentVersion.version)
append(", 'intelliJPlatformBuild' = ")
appendLine(currentVersion.build)
appendLine()
appendLine("That build number is for version ${match.version}.")
appendLine("Adjust the values so they're aligned correctly.")
}
)
}
// The match's build doesn't contain the -EAP-SNAPSHOT SUFFIX
if (currentVersion.type == "eap" && currentVersion.version != match.build) {
throw GradleException(
buildString {
appendLine("The 'idea' and 'intelliJPlatformBuild' properties in the catalog don't match.")
append("'idea' = ")
append(currentVersion.version)
append(", 'intelliJPlatformBuild' = ")
appendLine(currentVersion.build + "-EAP-SNAPSHOT")
appendLine()
appendLine("For non-stable IJP versions, the version and build should match,")
appendLine("minus the '-EAP-SNAPSHOT' suffix in the build number.")
appendLine("Adjust the values so they're aligned correctly.")
}
)
}
}
}

View File

@@ -0,0 +1,15 @@
package org.jetbrains.jewel.buildlogic.ideversion
import org.gradle.api.Project
val Project.currentIjpVersion: String
get() {
val rawValue =
property("ijp.target") as? String ?: error("Property ijp.target not defined. Check your gradle.properties!")
if (rawValue.length != 3 || rawValue.toIntOrNull()?.let { it < 0 } == true) {
error("Invalid ijp.target property value: '$rawValue'")
}
return rawValue
}

View File

@@ -0,0 +1,97 @@
package org.jetbrains.jewel.buildlogic.ideversion
import java.io.IOException
import java.net.URI
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.gradle.api.logging.Logger
internal object IJPVersionsFetcher {
fun fetchIJPVersions(releasesUrl: String, logger: Logger): List<ApiIdeaReleasesItem.Release>? {
val json = Json {
ignoreUnknownKeys = true
isLenient = true
}
val icReleases =
try {
URI.create(releasesUrl)
.toURL()
.openStream()
.use { json.decodeFromStream<List<ApiIdeaReleasesItem>>(it) }
.first()
} catch (e: IOException) {
logger.warn(
"Couldn't fetch IJ Platform releases, can't check for updates.\n" +
"Cause: ${e::class.qualifiedName}${e.message}"
)
return null
} catch (e: RuntimeException) {
logger.error("Unexpected error while fetching IJ Platform releases, can't check for updates.", e)
return null
}
check(icReleases.code == "IIC") { "Was expecting code IIC but was ${icReleases.code}" }
check(icReleases.releases.isNotEmpty()) { "Was expecting to have releases but the list is empty" }
return icReleases.releases
}
fun fetchBuildsForCurrentMajorVersion(
releasesUrl: String,
majorPlatformVersion: String,
logger: Logger,
): List<ApiIdeaReleasesItem.Release>? {
val releases = fetchIJPVersions(releasesUrl, logger) ?: return null
return releases
.asSequence()
.filter { it.majorVersion == majorPlatformVersion }
.sortedWith(ReleaseComparator)
.toList()
}
fun fetchLatestBuildForCurrentMajorVersion(releasesUrl: String, majorPlatformVersion: String, logger: Logger) =
fetchBuildsForCurrentMajorVersion(releasesUrl, majorPlatformVersion, logger)?.last()
fun compare(first: ApiIdeaReleasesItem.Release, second: ApiIdeaReleasesItem.Release): Int =
VersionComparator.compare(first.build, second.build)
private object ReleaseComparator : Comparator<ApiIdeaReleasesItem.Release> {
override fun compare(o1: ApiIdeaReleasesItem.Release?, o2: ApiIdeaReleasesItem.Release?): Int {
if (o1 == o2) return 0
if (o1 == null) return -1
if (o2 == null) return 1
return VersionComparator.compare(o1.build, o2.build)
}
}
private object VersionComparator : Comparator<String> {
override fun compare(o1: String?, o2: String?): Int {
if (o1 == o2) return 0
if (o1 == null) return -1
if (o2 == null) return 1
require(o1.isNotEmpty() && o1.all { it.isDigit() || it == '.' }) { "The first version is invalid: '$o1'" }
require(o2.isNotEmpty() && o2.all { it.isDigit() || it == '.' }) { "The first version is invalid: '$o2'" }
val firstGroups = o1.split('.')
val secondGroups = o2.split('.')
require(firstGroups.size == 3) { "The first version is invalid: '$o1'" }
require(secondGroups.size == 3) { "The second version is invalid: '$o2'" }
val firstComparison = firstGroups[0].toInt().compareTo(secondGroups[0].toInt())
if (firstComparison != 0) return firstComparison
val secondComparison = firstGroups[1].toInt().compareTo(secondGroups[1].toInt())
if (secondComparison != 0) return secondComparison
return firstGroups[2].toInt().compareTo(secondGroups[2].toInt())
}
}
}

View File

@@ -0,0 +1,137 @@
package org.jetbrains.jewel.buildlogic.theme
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.joinToCode
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
internal object IntUiThemeDescriptorReader {
private val colorGroups = setOf("Gray", "Blue", "Green", "Red", "Yellow", "Orange", "Purple", "Teal")
private val colorClassName = ClassName("androidx.compose.ui.graphics", "Color")
fun readThemeFrom(
themeDescriptor: IntellijThemeDescriptor,
className: ClassName,
ideaVersion: String,
descriptorUrl: String,
) =
FileSpec.builder(className)
.apply {
indent(" ")
addFileComment("Generated by the Jewel Int UI Palette Generator\n")
addFileComment("Generated from the IntelliJ Platform version $ideaVersion\n")
addFileComment("Source: $descriptorUrl")
addImport(colorClassName.packageName, colorClassName.simpleName)
addType(
TypeSpec.objectBuilder(className)
.apply {
addSuperinterface(
ClassName.bestGuess("org.jetbrains.jewel.foundation.theme.ThemeDescriptor")
)
addProperty(
PropertySpec.builder("isDark", Boolean::class, KModifier.OVERRIDE)
.initializer("%L", themeDescriptor.dark)
.build()
)
addProperty(
PropertySpec.builder("name", String::class, KModifier.OVERRIDE)
.initializer("\"%L (Int UI)\"", themeDescriptor.name)
.build()
)
readColors(themeDescriptor.colors)
readIcons(themeDescriptor)
}
.build()
)
}
.build()
private val colorPaletteClassName = ClassName.bestGuess("org.jetbrains.jewel.foundation.theme.ThemeColorPalette")
private val iconDataClassName = ClassName.bestGuess("org.jetbrains.jewel.foundation.theme.ThemeIconData")
private fun TypeSpec.Builder.readColors(colors: Map<String, String>) {
val colorGroups =
colors.entries
.groupBy { it.key.replace("""\d+""".toRegex(), "") }
.filterKeys { colorGroups.contains(it) }
.map { (groupName, colors) ->
// We assume color lists are in the same order as in colorGroups
colors
.map { (_, value) ->
val colorHexString = value.replace("#", "0xFF")
CodeBlock.of("Color(%L)", colorHexString)
}
.joinToCode(
prefix = "\n${groupName.lowercase()} = listOf(\n",
separator = ",\n",
suffix = "\n)",
)
}
val rawMap =
colors
.map { (key, value) ->
val colorHexString = value.replace("#", "0xFF")
CodeBlock.of("%S to Color(%L)", key, colorHexString)
}
.joinToCode(prefix = "\nrawMap = mapOf(\n", separator = ",\n", suffix = "\n)")
addProperty(
PropertySpec.builder("colors", colorPaletteClassName, KModifier.OVERRIDE)
.initializer("ThemeColorPalette(%L,\n%L\n)", colorGroups.joinToCode(","), rawMap)
.build()
)
}
private fun TypeSpec.Builder.readIcons(theme: IntellijThemeDescriptor) {
val iconOverrides = mutableMapOf<String, String>()
val colorPalette = mutableMapOf<String, String>()
for ((key, value) in theme.icons) {
if (value is JsonPrimitive && value.isString) {
iconOverrides += key to value.content
} else if (value is JsonObject && key == "ColorPalette") {
value.entries
.mapNotNull {
val pairValue = it.value
if (pairValue is JsonPrimitive && pairValue.isString) {
it.key to pairValue.content
} else null
}
.forEach { colorPalette[it.first] = it.second }
}
}
val iconOverridesBlock = iconOverrides.toMapCodeBlock()
val selectionColorPaletteBlock = theme.iconColorsOnSelection.toMapCodeBlock()
addProperty(
PropertySpec.builder("iconData", iconDataClassName, KModifier.OVERRIDE)
.initializer(
CodeBlock.of(
"ThemeIconData(iconOverrides = \n%L,colorPalette = \n%L,\nselectionColorPalette = %L\n)",
iconOverridesBlock,
colorPalette.toMapCodeBlock(),
selectionColorPaletteBlock,
)
)
.build()
)
}
private inline fun <reified K, reified V> Map<K, V>.toMapCodeBlock() =
entries
.map { (key, value) -> CodeBlock.of("\"%L\" to \"%L\"", key, value) }
.joinToCode(prefix = "mapOf(", separator = ",\n", suffix = ")")
}

View File

@@ -0,0 +1,80 @@
package org.jetbrains.jewel.buildlogic.theme
import com.squareup.kotlinpoet.ClassName
import java.net.URI
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromStream
import org.gradle.api.DefaultTask
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.property
class ThemeGeneratorContainer(container: NamedDomainObjectContainer<ThemeGeneration>) :
NamedDomainObjectContainer<ThemeGeneration> by container
class ThemeGeneration(val name: String, project: Project) {
val targetDir: DirectoryProperty =
project.objects.directoryProperty().convention(project.layout.buildDirectory.dir("generated/theme"))
val ideaVersion = project.objects.property<String>()
val themeClassName = project.objects.property<String>()
val themeFile = project.objects.property<String>()
}
open class IntelliJThemeGeneratorTask : DefaultTask() {
@get:OutputFile val outputFile: RegularFileProperty = project.objects.fileProperty()
@get:Input val ideaVersion = project.objects.property<String>()
@get:Input val themeFile = project.objects.property<String>()
@get:Input val themeClassName = project.objects.property<String>()
init {
group = "jewel"
}
@TaskAction
fun generate() {
val json = Json { ignoreUnknownKeys = true }
val url = buildString {
append("https://raw.githubusercontent.com/JetBrains/intellij-community/")
append(ideaVersion.get())
append("/")
append(themeFile.get())
}
logger.lifecycle("Fetching theme descriptor from $url...")
val themeDescriptor =
URI.create(url).toURL().openStream().use { json.decodeFromStream<IntellijThemeDescriptor>(it) }
val className = ClassName.bestGuess(themeClassName.get())
val file = IntUiThemeDescriptorReader.readThemeFrom(themeDescriptor, className, ideaVersion.get(), url)
val outputFile = outputFile.get().asFile
logger.lifecycle(
"Theme descriptor for ${themeDescriptor.name} parsed and " + "code generated into ${outputFile.path}"
)
outputFile.bufferedWriter().use { file.writeTo(it) }
}
}
@Serializable
data class IntellijThemeDescriptor(
val name: String,
val author: String = "",
val dark: Boolean = false,
val editorScheme: String,
val colors: Map<String, String> = emptyMap(),
val ui: Map<String, JsonElement> = emptyMap(),
val icons: Map<String, JsonElement> = emptyMap(),
val iconColorsOnSelection: Map<String, Int> = emptyMap(),
)

View File

@@ -0,0 +1,253 @@
public abstract interface class com/jetbrains/DesktopActions {
public abstract fun setHandler (Lcom/jetbrains/DesktopActions$Handler;)V
}
public abstract interface class com/jetbrains/DesktopActions$Handler {
public fun browse (Ljava/net/URI;)V
public fun edit (Ljava/io/File;)V
public fun mail (Ljava/net/URI;)V
public fun open (Ljava/io/File;)V
public fun print (Ljava/io/File;)V
}
public class com/jetbrains/JBR {
public static fun getApiVersion ()Ljava/lang/String;
public static fun getDesktopActions ()Lcom/jetbrains/DesktopActions;
public static fun getRoundedCornersManager ()Lcom/jetbrains/RoundedCornersManager;
public static fun getWindowDecorations ()Lcom/jetbrains/WindowDecorations;
public static fun getWindowMove ()Lcom/jetbrains/WindowMove;
public static fun isAvailable ()Z
public static fun isDesktopActionsSupported ()Z
public static fun isRoundedCornersManagerSupported ()Z
public static fun isWindowDecorationsSupported ()Z
public static fun isWindowMoveSupported ()Z
}
public abstract interface class com/jetbrains/RoundedCornersManager {
public abstract fun setRoundedCorners (Ljava/awt/Window;Ljava/lang/Object;)V
}
public abstract interface class com/jetbrains/WindowDecorations {
public abstract fun createCustomTitleBar ()Lcom/jetbrains/WindowDecorations$CustomTitleBar;
public abstract fun setCustomTitleBar (Ljava/awt/Dialog;Lcom/jetbrains/WindowDecorations$CustomTitleBar;)V
public abstract fun setCustomTitleBar (Ljava/awt/Frame;Lcom/jetbrains/WindowDecorations$CustomTitleBar;)V
}
public abstract interface class com/jetbrains/WindowDecorations$CustomTitleBar {
public abstract fun forceHitTest (Z)V
public abstract fun getContainingWindow ()Ljava/awt/Window;
public abstract fun getHeight ()F
public abstract fun getLeftInset ()F
public abstract fun getProperties ()Ljava/util/Map;
public abstract fun getRightInset ()F
public abstract fun putProperties (Ljava/util/Map;)V
public abstract fun putProperty (Ljava/lang/String;Ljava/lang/Object;)V
public abstract fun setHeight (F)V
}
public abstract interface class com/jetbrains/WindowMove {
public abstract fun startMovingTogetherWithMouse (Ljava/awt/Window;I)V
}
public final class org/jetbrains/jewel/window/DecoratedWindowKt {
public static final fun DecoratedWindow (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/window/WindowState;ZLjava/lang/String;Landroidx/compose/ui/graphics/painter/Painter;ZZZZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/jewel/window/styling/DecoratedWindowStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
}
public abstract interface class org/jetbrains/jewel/window/DecoratedWindowScope : androidx/compose/ui/window/FrameWindowScope {
public abstract fun getState-VA8cQZQ ()J
public abstract fun getWindow ()Landroidx/compose/ui/awt/ComposeWindow;
}
public final class org/jetbrains/jewel/window/DecoratedWindowState {
public static final field Companion Lorg/jetbrains/jewel/window/DecoratedWindowState$Companion;
public static final synthetic fun box-impl (J)Lorg/jetbrains/jewel/window/DecoratedWindowState;
public static fun constructor-impl (J)J
public static final fun copy-zAQEbgo (JZZZZ)J
public static synthetic fun copy-zAQEbgo$default (JZZZZILjava/lang/Object;)J
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (JLjava/lang/Object;)Z
public static final fun equals-impl0 (JJ)Z
public final fun getState-s-VKNKU ()J
public fun hashCode ()I
public static fun hashCode-impl (J)I
public static final fun isActive-impl (J)Z
public static final fun isFullscreen-impl (J)Z
public static final fun isMaximized-impl (J)Z
public static final fun isMinimized-impl (J)Z
public fun toString ()Ljava/lang/String;
public static fun toString-impl (J)Ljava/lang/String;
public final synthetic fun unbox-impl ()J
}
public final class org/jetbrains/jewel/window/DecoratedWindowState$Companion {
public final fun getActive-s-VKNKU ()J
public final fun getFullscreen-s-VKNKU ()J
public final fun getMaximize-s-VKNKU ()J
public final fun getMinimize-s-VKNKU ()J
public final fun of-LPCgXDc (Landroidx/compose/ui/awt/ComposeWindow;)J
public final fun of-zAQEbgo (ZZZZ)J
public static synthetic fun of-zAQEbgo$default (Lorg/jetbrains/jewel/window/DecoratedWindowState$Companion;ZZZZILjava/lang/Object;)J
}
public final class org/jetbrains/jewel/window/ThemeKt {
public static final fun getDefaultDecoratedWindowStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/window/styling/DecoratedWindowStyle;
public static final fun getDefaultTitleBarStyle (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/window/styling/TitleBarStyle;
}
public final class org/jetbrains/jewel/window/TitleBarKt {
public static final fun TitleBar-T042LqI (Lorg/jetbrains/jewel/window/DecoratedWindowScope;Landroidx/compose/ui/Modifier;JLorg/jetbrains/jewel/window/styling/TitleBarStyle;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
}
public abstract interface class org/jetbrains/jewel/window/TitleBarScope {
public abstract fun align (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment$Horizontal;)Landroidx/compose/ui/Modifier;
public abstract fun getIcon ()Landroidx/compose/ui/graphics/painter/Painter;
public abstract fun getTitle ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/TitleBar_MacOSKt {
public static final fun newFullscreenControls (Landroidx/compose/ui/Modifier;Z)Landroidx/compose/ui/Modifier;
public static synthetic fun newFullscreenControls$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier;
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowColors {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/DecoratedWindowColors$Companion;
public synthetic fun <init> (JJLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun borderFor-3hEOMOc (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public fun equals (Ljava/lang/Object;)Z
public final fun getBorder-0d7_KjU ()J
public final fun getBorderInactive-0d7_KjU ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowColors$Companion {
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowMetrics {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/DecoratedWindowMetrics$Companion;
public synthetic fun <init> (FLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getBorderWidth-D9Ej5fM ()F
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowMetrics$Companion {
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowStyle {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/DecoratedWindowStyle$Companion;
public fun <init> (Lorg/jetbrains/jewel/window/styling/DecoratedWindowColors;Lorg/jetbrains/jewel/window/styling/DecoratedWindowMetrics;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getColors ()Lorg/jetbrains/jewel/window/styling/DecoratedWindowColors;
public final fun getMetrics ()Lorg/jetbrains/jewel/window/styling/DecoratedWindowMetrics;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowStyle$Companion {
}
public final class org/jetbrains/jewel/window/styling/DecoratedWindowStylingKt {
public static final fun getLocalDecoratedWindowStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}
public final class org/jetbrains/jewel/window/styling/TitleBarColors {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarColors$Companion;
public synthetic fun <init> (JJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun backgroundFor-3hEOMOc (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public fun equals (Ljava/lang/Object;)Z
public final fun getBackground-0d7_KjU ()J
public final fun getBorder-0d7_KjU ()J
public final fun getContent-0d7_KjU ()J
public final fun getDropdownHoveredBackground-0d7_KjU ()J
public final fun getDropdownPressedBackground-0d7_KjU ()J
public final fun getFullscreenControlButtonsBackground-0d7_KjU ()J
public final fun getIconButtonHoveredBackground-0d7_KjU ()J
public final fun getIconButtonPressedBackground-0d7_KjU ()J
public final fun getInactiveBackground-0d7_KjU ()J
public final fun getTitlePaneButtonHoveredBackground-0d7_KjU ()J
public final fun getTitlePaneButtonPressedBackground-0d7_KjU ()J
public final fun getTitlePaneCloseButtonHoveredBackground-0d7_KjU ()J
public final fun getTitlePaneCloseButtonPressedBackground-0d7_KjU ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/TitleBarColors$Companion {
}
public final class org/jetbrains/jewel/window/styling/TitleBarIcons {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarIcons$Companion;
public fun <init> (Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getCloseButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
public final fun getMaximizeButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
public final fun getMinimizeButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
public final fun getRestoreButton ()Lorg/jetbrains/jewel/ui/icon/IconKey;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/TitleBarIcons$Companion {
}
public final class org/jetbrains/jewel/window/styling/TitleBarMetrics {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarMetrics$Companion;
public synthetic fun <init> (FFFJLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getGradientEndX-D9Ej5fM ()F
public final fun getGradientStartX-D9Ej5fM ()F
public final fun getHeight-D9Ej5fM ()F
public final fun getTitlePaneButtonSize-MYxV2XQ ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/TitleBarMetrics$Companion {
}
public final class org/jetbrains/jewel/window/styling/TitleBarStyle {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/window/styling/TitleBarStyle$Companion;
public fun <init> (Lorg/jetbrains/jewel/window/styling/TitleBarColors;Lorg/jetbrains/jewel/window/styling/TitleBarMetrics;Lorg/jetbrains/jewel/window/styling/TitleBarIcons;Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getColors ()Lorg/jetbrains/jewel/window/styling/TitleBarColors;
public final fun getDropdownStyle ()Lorg/jetbrains/jewel/ui/component/styling/DropdownStyle;
public final fun getIconButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;
public final fun getIcons ()Lorg/jetbrains/jewel/window/styling/TitleBarIcons;
public final fun getMetrics ()Lorg/jetbrains/jewel/window/styling/TitleBarMetrics;
public final fun getPaneButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;
public final fun getPaneCloseButtonStyle ()Lorg/jetbrains/jewel/ui/component/styling/IconButtonStyle;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class org/jetbrains/jewel/window/styling/TitleBarStyle$Companion {
}
public final class org/jetbrains/jewel/window/styling/TitleBarStylingKt {
public static final fun getLocalTitleBarStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}
public final class org/jetbrains/jewel/window/utils/DesktopPlatform : java/lang/Enum {
public static final field Companion Lorg/jetbrains/jewel/window/utils/DesktopPlatform$Companion;
public static final field Linux Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
public static final field MacOS Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
public static final field Unknown Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
public static final field Windows Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
public static fun values ()[Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
}
public final class org/jetbrains/jewel/window/utils/DesktopPlatform$Companion {
public final fun getCurrent ()Lorg/jetbrains/jewel/window/utils/DesktopPlatform;
}

View File

@@ -0,0 +1,18 @@
import org.jetbrains.compose.ComposeBuildConfig
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
alias(libs.plugins.compose.compiler)
}
private val composeVersion
get() = ComposeBuildConfig.composeVersion
dependencies {
api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")
api(projects.ui)
implementation(libs.jna.core)
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2000-2022 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jetbrains;
import java.io.File;
import java.io.IOException;
import java.net.URI;
public interface DesktopActions {
void setHandler(Handler handler);
interface Handler {
default void open(File file) throws IOException { throw new UnsupportedOperationException(); }
default void edit(File file) throws IOException { throw new UnsupportedOperationException(); }
default void print(File file) throws IOException { throw new UnsupportedOperationException(); }
default void mail(URI mailtoURL) throws IOException { throw new UnsupportedOperationException(); }
default void browse(URI uri) throws IOException { throw new UnsupportedOperationException(); }
}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright 2000-2023 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jetbrains;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
/**
* This class is an entry point into JBR API.
* JBR API is a collection of services, classes, interfaces, etc.,
* which require tight interaction with JRE and therefore are implemented inside JBR.
* <div>JBR API consists of two parts:</div>
* <ul>
* <li>Client side - {@code jetbrains.api} module, mostly containing interfaces</li>
* <li>JBR side - actual implementation code inside JBR</li>
* </ul>
* Client and JBR side are linked dynamically at runtime and do not have to be of the same version.
* In some cases (e.g. running on different JRE or old JBR) system will not be able to find
* implementation for some services, so you'll need a fallback behavior for that case.
* <h2>Simple usage example:</h2>
* <blockquote><pre>{@code
* if (JBR.isSomeServiceSupported()) {
* JBR.getSomeService().doSomething();
* } else {
* planB();
* }
* }</pre></blockquote>
*
* @implNote JBR API is initialized on first access to this class (in static initializer).
* Actual implementation is linked on demand, when corresponding service is requested by client.
*/
public class JBR {
private static final ServiceApi api;
private static final Exception bootstrapException;
static {
ServiceApi a = null;
Exception exception = null;
try {
a = (ServiceApi) Class.forName("com.jetbrains.bootstrap.JBRApiBootstrap")
.getMethod("bootstrap", MethodHandles.Lookup.class)
.invoke(null, MethodHandles.lookup());
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof Error error) throw error;
else throw new Error(t);
} catch (IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
exception = e;
}
api = a;
bootstrapException = exception;
}
private JBR() {
}
private static <T> T getService(Class<T> interFace, FallbackSupplier<T> fallback) {
T service = getService(interFace);
try {
return service != null ? service : fallback != null ? fallback.get() : null;
} catch (Throwable ignore) {
return null;
}
}
static <T> T getService(Class<T> interFace) {
return api == null ? null : api.getService(interFace);
}
/**
* @return true when running on JBR which implements JBR API
*/
public static boolean isAvailable() {
return api != null;
}
/**
* @return JBR API version in form {@code JBR.MAJOR.MINOR.PATCH}
* @implNote This is an API version, which comes with client application,
* it has nothing to do with JRE it runs on.
*/
public static String getApiVersion() {
return "17.0.8.1b1070.2.1.9.0";
}
/**
* Internal API interface, contains most basic methods for communication between client and JBR.
*/
private interface ServiceApi {
<T> T getService(Class<T> interFace);
}
@FunctionalInterface
private interface FallbackSupplier<T> {
T get() throws Throwable;
}
// ========================== Generated metadata ==========================
/**
* Generated client-side metadata, needed by JBR when linking the implementation.
*/
private static final class Metadata {
private static final String[] KNOWN_SERVICES = {"com.jetbrains.ExtendedGlyphCache", "com.jetbrains.DesktopActions", "com.jetbrains.CustomWindowDecoration", "com.jetbrains.ProjectorUtils", "com.jetbrains.FontExtensions", "com.jetbrains.RoundedCornersManager", "com.jetbrains.GraphicsUtils", "com.jetbrains.WindowDecorations", "com.jetbrains.JBRFileDialogService", "com.jetbrains.AccessibleAnnouncer", "com.jetbrains.JBR$ServiceApi", "com.jetbrains.Jstack", "com.jetbrains.WindowMove"};
private static final String[] KNOWN_PROXIES = {"com.jetbrains.JBRFileDialog", "com.jetbrains.WindowDecorations$CustomTitleBar"};
}
// ======================= Generated static methods =======================
private static class DesktopActions__Holder {
private static final DesktopActions INSTANCE = getService(DesktopActions.class, null);
}
/**
* @return true if current runtime has implementation for all methods in {@link DesktopActions}
* and its dependencies (can fully implement given service).
* @see #getDesktopActions()
*/
public static boolean isDesktopActionsSupported() {
return DesktopActions__Holder.INSTANCE != null;
}
/**
* @return full implementation of {@link DesktopActions} service if any, or {@code null} otherwise
*/
public static DesktopActions getDesktopActions() {
return DesktopActions__Holder.INSTANCE;
}
private static class RoundedCornersManager__Holder {
private static final RoundedCornersManager INSTANCE = getService(RoundedCornersManager.class, null);
}
/**
* @return true if current runtime has implementation for all methods in {@link RoundedCornersManager}
* and its dependencies (can fully implement given service).
* @see #getRoundedCornersManager()
*/
public static boolean isRoundedCornersManagerSupported() {
return RoundedCornersManager__Holder.INSTANCE != null;
}
/**
* This manager allows decorate awt Window with rounded corners.
* Appearance depends from operating system.
*
* @return full implementation of {@link RoundedCornersManager} service if any, or {@code null} otherwise
*/
public static RoundedCornersManager getRoundedCornersManager() {
return RoundedCornersManager__Holder.INSTANCE;
}
private static class WindowDecorations__Holder {
private static final WindowDecorations INSTANCE = getService(WindowDecorations.class, null);
}
/**
* @return true if current runtime has implementation for all methods in {@link WindowDecorations}
* and its dependencies (can fully implement given service).
* @see #getWindowDecorations()
*/
public static boolean isWindowDecorationsSupported() {
return WindowDecorations__Holder.INSTANCE != null;
}
/**
* Window decorations consist of title bar, window controls and border.
*
* @return full implementation of {@link WindowDecorations} service if any, or {@code null} otherwise
* @see WindowDecorations.CustomTitleBar
*/
public static WindowDecorations getWindowDecorations() {
return WindowDecorations__Holder.INSTANCE;
}
private static class WindowMove__Holder {
private static final WindowMove INSTANCE = getService(WindowMove.class, null);
}
/**
* @return true if current runtime has implementation for all methods in {@link WindowMove}
* and its dependencies (can fully implement given service).
* @see #getWindowMove()
*/
public static boolean isWindowMoveSupported() {
return WindowMove__Holder.INSTANCE != null;
}
/**
* @return full implementation of {@link WindowMove} service if any, or {@code null} otherwise
*/
public static WindowMove getWindowMove() {
return WindowMove__Holder.INSTANCE;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2000-2023 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jetbrains;
import java.awt.*;
/**
* This manager allows decorate awt Window with rounded corners.
* Appearance depends from operating system.
*/
public interface RoundedCornersManager {
/**
* @param params for macOS is Float object with radius or
* Array with {Float for radius, Integer for border width, java.awt.Color for border color}.
*
* @param params for Windows 11 is String with values:
* "default" - let the system decide whether or not to round window corners,
* "none" - never round window corners,
* "full" - round the corners if appropriate,
* "small" - round the corners if appropriate, with a small radius.
*/
void setRoundedCorners(Window window, Object params);
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2023 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jetbrains;
import java.awt.*;
import java.util.Map;
/**
* Window decorations consist of title bar, window controls and border.
* @see CustomTitleBar
*/
public interface WindowDecorations {
/**
* If {@code customTitleBar} is not null, system-provided title bar is removed and client area is extended to the
* top of the frame with window controls painted over the client area.
* {@code customTitleBar=null} resets to the default appearance with system-provided title bar.
* @see CustomTitleBar
* @see #createCustomTitleBar()
*/
void setCustomTitleBar(Frame frame, CustomTitleBar customTitleBar);
/**
* If {@code customTitleBar} is not null, system-provided title bar is removed and client area is extended to the
* top of the dialog with window controls painted over the client area.
* {@code customTitleBar=null} resets to the default appearance with system-provided title bar.
* @see CustomTitleBar
* @see #createCustomTitleBar()
*/
void setCustomTitleBar(Dialog dialog, CustomTitleBar customTitleBar);
/**
* You must {@linkplain CustomTitleBar#setHeight(float) set title bar height} before adding it to a window.
* @see CustomTitleBar
* @see #setCustomTitleBar(Frame, CustomTitleBar)
* @see #setCustomTitleBar(Dialog, CustomTitleBar)
*/
CustomTitleBar createCustomTitleBar();
/**
* Custom title bar allows merging of window content with native title bar,
* which is done by treating title bar as part of client area, but with some
* special behavior like dragging or maximizing on double click.
* Custom title bar has {@linkplain CustomTitleBar#getHeight() height} and controls.
* @implNote Behavior is platform-dependent, only macOS and Windows are supported.
* @see #setCustomTitleBar(Frame, CustomTitleBar)
*/
interface CustomTitleBar {
/**
* @return title bar height, measured in pixels from the top of client area, i.e. excluding top frame border.
*/
float getHeight();
/**
* @param height title bar height, measured in pixels from the top of client area,
* i.e. excluding top frame border. Must be > 0.
*/
void setHeight(float height);
/**
* @see #putProperty(String, Object)
*/
Map<String, Object> getProperties();
/**
* @see #putProperty(String, Object)
*/
void putProperties(Map<String, ?> m);
/**
* Windows & macOS properties:
* <ul>
* <li>{@code controls.visible} : {@link Boolean} - whether title bar controls
* (minimize/maximize/close buttons) are visible, default = true.</li>
* </ul>
* Windows properties:
* <ul>
* <li>{@code controls.width} : {@link Number} - width of block of buttons (not individual buttons).
* Note that dialogs have only one button, while frames usually have 3 of them.</li>
* <li>{@code controls.dark} : {@link Boolean} - whether to use dark or light color theme
* (light or dark icons respectively).</li>
* <li>{@code controls.<layer>.<state>} : {@link Color} - precise control over button colors,
* where {@code <layer>} is one of:
* <ul><li>{@code foreground}</li><li>{@code background}</li></ul>
* and {@code <state>} is one of:
* <ul>
* <li>{@code normal}</li>
* <li>{@code hovered}</li>
* <li>{@code pressed}</li>
* <li>{@code disabled}</li>
* <li>{@code inactive}</li>
* </ul>
* </ul>
*/
void putProperty(String key, Object value);
/**
* @return space occupied by title bar controls on the left (px)
*/
float getLeftInset();
/**
* @return space occupied by title bar controls on the right (px)
*/
float getRightInset();
/**
* By default, any component which has no cursor or mouse event listeners set is considered transparent for
* native title bar actions. That is, dragging simple JPanel in title bar area will drag the
* window, but dragging a JButton will not. Adding mouse listener to a component will prevent any native actions
* inside bounds of that component.
* <p>
* This method gives you precise control of whether to allow native title bar actions or not.
* <ul>
* <li>{@code client=true} means that mouse is currently over a client area. Native title bar behavior is disabled.</li>
* <li>{@code client=false} means that mouse is currently over a non-client area. Native title bar behavior is enabled.</li>
* </ul>
* <em>Intended usage:
* <ul>
* <li>This method must be called in response to all {@linkplain java.awt.event.MouseEvent mouse events}
* except {@link java.awt.event.MouseEvent#MOUSE_EXITED} and {@link java.awt.event.MouseEvent#MOUSE_WHEEL}.</li>
* <li>This method is called per-event, i.e. when component has multiple listeners, you only need to call it once.</li>
* <li>If this method hadn't been called, title bar behavior is reverted back to default upon processing the event.</li>
* </ul></em>
* Note that hit test value is relevant only for title bar area, e.g. calling
* {@code forceHitTest(false)} will not make window draggable via non-title bar area.
*
* <h2>Example:</h2>
* Suppose you have a {@code JPanel} in the title bar area. You want it to respond to right-click for
* some popup menu, but also retain native drag and double-click behavior.
* <pre>
* CustomTitleBar titlebar = ...;
* JPanel panel = ...;
* MouseAdapter adapter = new MouseAdapter() {
* private void hit() { titlebar.forceHitTest(false); }
* public void mouseClicked(MouseEvent e) {
* hit();
* if (e.getButton() == MouseEvent.BUTTON3) ...;
* }
* public void mousePressed(MouseEvent e) { hit(); }
* public void mouseReleased(MouseEvent e) { hit(); }
* public void mouseEntered(MouseEvent e) { hit(); }
* public void mouseDragged(MouseEvent e) { hit(); }
* public void mouseMoved(MouseEvent e) { hit(); }
* };
* panel.addMouseListener(adapter);
* panel.addMouseMotionListener(adapter);
* </pre>
*/
void forceHitTest(boolean client);
Window getContainingWindow();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2000-2023 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.jetbrains;
import java.awt.*;
public interface WindowMove {
/**
* Starts moving the top-level parent window of the given window together with the mouse pointer.
* The intended use is to facilitate the implementation of window management similar to the way
* it is done natively on the platform.
*
* Preconditions for calling this method:
* <ul>
* <li>WM supports _NET_WM_MOVE_RESIZE (this is checked automatically when an implementation
* of this interface is obtained).</li>
* <li>Mouse pointer is within this window's bounds.</li>
* <li>The mouse button specified by {@code mouseButton} is pressed.</li>
* </ul>
*
* Calling this method will make the window start moving together with the mouse pointer until
* the specified mouse button is released or Esc is pressed. The conditions for cancelling
* the move may differ between WMs.
*
* @param mouseButton indicates the mouse button that was pressed to start moving the window;
* must be one of {@code MouseEvent.BUTTON1}, {@code MouseEvent.BUTTON2},
* or {@code MouseEvent.BUTTON3}.
*/
void startMovingTogetherWithMouse(Window window, int mouseButton);
}

View File

@@ -0,0 +1,278 @@
package org.jetbrains.jewel.window
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
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.awt.ComposeWindow
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.offset
import androidx.compose.ui.window.FrameWindowScope
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.rememberWindowState
import com.jetbrains.JBR
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
import org.jetbrains.jewel.foundation.modifier.trackWindowActivation
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.window.styling.DecoratedWindowStyle
import org.jetbrains.jewel.window.utils.DesktopPlatform
@Composable
public fun DecoratedWindow(
onCloseRequest: () -> Unit,
state: WindowState = rememberWindowState(),
visible: Boolean = true,
title: String = "",
icon: Painter? = null,
resizable: Boolean = true,
enabled: Boolean = true,
focusable: Boolean = true,
alwaysOnTop: Boolean = false,
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
onKeyEvent: (KeyEvent) -> Boolean = { false },
style: DecoratedWindowStyle = JewelTheme.defaultDecoratedWindowStyle,
content: @Composable DecoratedWindowScope.() -> Unit,
) {
remember {
if (!JBR.isAvailable()) {
error(
"DecoratedWindow can only be used on JetBrainsRuntime(JBR) platform, " +
"please check the document https://github.com/JetBrains/jewel#int-ui-standalone-theme"
)
}
}
// Using undecorated window for linux
val undecorated = DesktopPlatform.Linux == DesktopPlatform.Current
Window(
onCloseRequest,
state,
visible,
title,
icon,
undecorated,
transparent = false,
resizable,
enabled,
focusable,
alwaysOnTop,
onPreviewKeyEvent,
onKeyEvent,
) {
var decoratedWindowState by remember { mutableStateOf(DecoratedWindowState.of(window)) }
DisposableEffect(window) {
val adapter =
object : WindowAdapter(), ComponentListener {
override fun windowActivated(e: WindowEvent?) {
decoratedWindowState = DecoratedWindowState.of(window)
}
override fun windowDeactivated(e: WindowEvent?) {
decoratedWindowState = DecoratedWindowState.of(window)
}
override fun windowIconified(e: WindowEvent?) {
decoratedWindowState = DecoratedWindowState.of(window)
}
override fun windowDeiconified(e: WindowEvent?) {
decoratedWindowState = DecoratedWindowState.of(window)
}
override fun windowStateChanged(e: WindowEvent) {
decoratedWindowState = DecoratedWindowState.of(window)
}
override fun componentResized(e: ComponentEvent?) {
decoratedWindowState = DecoratedWindowState.of(window)
}
override fun componentMoved(e: ComponentEvent?) {
// Empty
}
override fun componentShown(e: ComponentEvent?) {
// Empty
}
override fun componentHidden(e: ComponentEvent?) {
// Empty
}
}
window.addWindowListener(adapter)
window.addWindowStateListener(adapter)
window.addComponentListener(adapter)
onDispose {
window.removeWindowListener(adapter)
window.removeWindowStateListener(adapter)
window.removeComponentListener(adapter)
}
}
val undecoratedWindowBorder =
if (undecorated && !decoratedWindowState.isMaximized) {
Modifier.border(
Stroke.Alignment.Inside,
style.metrics.borderWidth,
style.colors.borderFor(decoratedWindowState).value,
RectangleShape,
)
.padding(style.metrics.borderWidth)
} else {
Modifier
}
CompositionLocalProvider(LocalTitleBarInfo provides TitleBarInfo(title, icon)) {
Layout(
content = {
val scope =
object : DecoratedWindowScope {
override val state: DecoratedWindowState
get() = decoratedWindowState
override val window: ComposeWindow
get() = this@Window.window
}
scope.content()
},
modifier = undecoratedWindowBorder.trackWindowActivation(window),
measurePolicy = DecoratedWindowMeasurePolicy,
)
}
}
}
@Stable
public interface DecoratedWindowScope : FrameWindowScope {
override val window: ComposeWindow
public val state: DecoratedWindowState
}
private object DecoratedWindowMeasurePolicy : MeasurePolicy {
override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
if (measurables.isEmpty()) {
return layout(width = constraints.minWidth, height = constraints.minHeight) {}
}
val titleBars = measurables.filter { it.layoutId == TITLE_BAR_LAYOUT_ID }
if (titleBars.size > 1) {
error("Window just can have only one title bar")
}
val titleBar = titleBars.firstOrNull()
val titleBarBorder = measurables.firstOrNull { it.layoutId == TITLE_BAR_BORDER_LAYOUT_ID }
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val titleBarPlaceable = titleBar?.measure(contentConstraints)
val titleBarHeight = titleBarPlaceable?.height ?: 0
val titleBarBorderPlaceable = titleBarBorder?.measure(contentConstraints)
val titleBarBorderHeight = titleBarBorderPlaceable?.height ?: 0
val measuredPlaceable = mutableListOf<Placeable>()
for (it in measurables) {
if (it.layoutId.toString().startsWith(TITLE_BAR_COMPONENT_LAYOUT_ID_PREFIX)) continue
val offsetConstraints = contentConstraints.offset(vertical = -titleBarHeight - titleBarBorderHeight)
val placeable = it.measure(offsetConstraints)
measuredPlaceable += placeable
}
return layout(constraints.maxWidth, constraints.maxHeight) {
titleBarPlaceable?.placeRelative(0, 0)
titleBarBorderPlaceable?.placeRelative(0, titleBarHeight)
measuredPlaceable.forEach { it.placeRelative(0, titleBarHeight + titleBarBorderHeight) }
}
}
}
@Immutable
@JvmInline
public value class DecoratedWindowState(public val state: ULong) {
public val isActive: Boolean
get() = state and Active != 0UL
public val isFullscreen: Boolean
get() = state and Fullscreen != 0UL
public val isMinimized: Boolean
get() = state and Minimize != 0UL
public val isMaximized: Boolean
get() = state and Maximize != 0UL
public fun copy(
fullscreen: Boolean = isFullscreen,
minimized: Boolean = isMinimized,
maximized: Boolean = isMaximized,
active: Boolean = isActive,
): DecoratedWindowState = of(fullscreen = fullscreen, minimized = minimized, maximized = maximized, active = active)
override fun toString(): String = "${javaClass.simpleName}(isFullscreen=$isFullscreen, isActive=$isActive)"
public companion object {
public val Active: ULong = 1UL shl 0
public val Fullscreen: ULong = 1UL shl 1
public val Minimize: ULong = 1UL shl 2
public val Maximize: ULong = 1UL shl 3
public fun of(
fullscreen: Boolean = false,
minimized: Boolean = false,
maximized: Boolean = false,
active: Boolean = true,
): DecoratedWindowState =
DecoratedWindowState(
(if (fullscreen) Fullscreen else 0UL) or
(if (minimized) Minimize else 0UL) or
(if (maximized) Maximize else 0UL) or
(if (active) Active else 0UL)
)
public fun of(window: ComposeWindow): DecoratedWindowState =
of(
fullscreen = window.placement == WindowPlacement.Fullscreen,
minimized = window.isMinimized,
maximized = window.placement == WindowPlacement.Maximized,
active = window.isActive,
)
}
}
internal data class TitleBarInfo(val title: String, val icon: Painter?)
internal val LocalTitleBarInfo: ProvidableCompositionLocal<TitleBarInfo> = compositionLocalOf {
error("LocalTitleBarInfo not provided, TitleBar must be used in DecoratedWindow")
}

View File

@@ -0,0 +1,15 @@
package org.jetbrains.jewel.window
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.window.styling.DecoratedWindowStyle
import org.jetbrains.jewel.window.styling.LocalDecoratedWindowStyle
import org.jetbrains.jewel.window.styling.LocalTitleBarStyle
import org.jetbrains.jewel.window.styling.TitleBarStyle
public val JewelTheme.Companion.defaultTitleBarStyle: TitleBarStyle
@Composable @ReadOnlyComposable get() = LocalTitleBarStyle.current
public val JewelTheme.Companion.defaultDecoratedWindowStyle: DecoratedWindowStyle
@Composable @ReadOnlyComposable get() = LocalDecoratedWindowStyle.current

View File

@@ -0,0 +1,109 @@
package org.jetbrains.jewel.window
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.dp
import com.jetbrains.JBR
import java.awt.Frame
import java.awt.event.MouseEvent
import java.awt.event.WindowEvent
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.IconButton
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
import org.jetbrains.jewel.ui.painter.PainterSuffixHint
import org.jetbrains.jewel.window.styling.TitleBarStyle
@Composable
internal fun DecoratedWindowScope.TitleBarOnLinux(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
var lastPress = 0L
val viewConfig = LocalViewConfiguration.current
TitleBarImpl(
modifier.onPointerEvent(PointerEventType.Press, PointerEventPass.Main) {
if (
this.currentEvent.button == PointerButton.Primary &&
this.currentEvent.changes.any { changed -> !changed.isConsumed }
) {
JBR.getWindowMove()?.startMovingTogetherWithMouse(window, MouseEvent.BUTTON1)
if (
System.currentTimeMillis() - lastPress in
viewConfig.doubleTapMinTimeMillis..viewConfig.doubleTapTimeoutMillis
) {
if (state.isMaximized) {
window.extendedState = Frame.NORMAL
} else {
window.extendedState = Frame.MAXIMIZED_BOTH
}
}
lastPress = System.currentTimeMillis()
}
},
gradientStartColor,
style,
{ _, _ -> PaddingValues(0.dp) },
) { state ->
CloseButton({ window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) }, state, style)
if (state.isMaximized) {
ControlButton({ window.extendedState = Frame.NORMAL }, state, style.icons.restoreButton, "Restore")
} else {
ControlButton(
{ window.extendedState = Frame.MAXIMIZED_BOTH },
state,
style.icons.maximizeButton,
"Maximize",
)
}
ControlButton({ window.extendedState = Frame.ICONIFIED }, state, style.icons.minimizeButton, "Minimize")
content(state)
}
}
@Composable
private fun TitleBarScope.CloseButton(
onClick: () -> Unit,
state: DecoratedWindowState,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
) {
ControlButton(onClick, state, style.icons.closeButton, "Close", style, style.paneCloseButtonStyle)
}
@Composable
private fun TitleBarScope.ControlButton(
onClick: () -> Unit,
state: DecoratedWindowState,
iconKey: IconKey,
description: String,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
iconButtonStyle: IconButtonStyle = style.paneButtonStyle,
) {
IconButton(
onClick,
Modifier.align(Alignment.End).focusable(false).size(style.metrics.titlePaneButtonSize),
style = iconButtonStyle,
) {
Icon(iconKey, description, hint = if (state.isActive) PainterHint else Inactive)
}
}
private data object Inactive : PainterSuffixHint() {
override fun PainterProviderScope.suffix(): String = "Inactive"
}

View File

@@ -0,0 +1,100 @@
package org.jetbrains.jewel.window
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import com.jetbrains.JBR
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.window.styling.TitleBarStyle
import org.jetbrains.jewel.window.utils.macos.MacUtil
public fun Modifier.newFullscreenControls(newControls: Boolean = true): Modifier =
this then
NewFullscreenControlsElement(
newControls,
debugInspectorInfo {
name = "newFullscreenControls"
value = newControls
},
)
private class NewFullscreenControlsElement(val newControls: Boolean, val inspectorInfo: InspectorInfo.() -> Unit) :
ModifierNodeElement<NewFullscreenControlsNode>() {
override fun create(): NewFullscreenControlsNode = NewFullscreenControlsNode(newControls)
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? NewFullscreenControlsElement ?: return false
return newControls == otherModifier.newControls
}
override fun hashCode(): Int = newControls.hashCode()
override fun InspectorInfo.inspectableProperties() {
inspectorInfo()
}
override fun update(node: NewFullscreenControlsNode) {
node.newControls = newControls
}
}
private class NewFullscreenControlsNode(var newControls: Boolean) : Modifier.Node()
@Composable
internal fun DecoratedWindowScope.TitleBarOnMacOs(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
val newFullscreenControls =
modifier.foldOut(false) { e, r ->
if (e is NewFullscreenControlsElement) {
e.newControls
} else {
r
}
}
if (newFullscreenControls) {
System.setProperty("apple.awt.newFullScreeControls", true.toString())
System.setProperty(
"apple.awt.newFullScreeControls.background",
"${style.colors.fullscreenControlButtonsBackground.toArgb()}",
)
MacUtil.updateColors(window)
} else {
System.clearProperty("apple.awt.newFullScreeControls")
System.clearProperty("apple.awt.newFullScreeControls.background")
}
val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() }
TitleBarImpl(
modifier = modifier.customTitleBarMouseEventHandler(titleBar),
gradientStartColor = gradientStartColor,
style = style,
applyTitleBar = { height, state ->
if (state.isFullscreen) {
MacUtil.updateFullScreenButtons(window)
}
titleBar.height = height.value
JBR.getWindowDecorations().setCustomTitleBar(window, titleBar)
if (state.isFullscreen && newFullscreenControls) {
PaddingValues(start = 80.dp)
} else {
PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp)
}
},
content = content,
)
}

View File

@@ -0,0 +1,65 @@
package org.jetbrains.jewel.window
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import com.jetbrains.JBR
import com.jetbrains.WindowDecorations.CustomTitleBar
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.util.isDark
import org.jetbrains.jewel.window.styling.TitleBarStyle
@Composable
internal fun DecoratedWindowScope.TitleBarOnWindows(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() }
TitleBarImpl(
modifier = modifier.customTitleBarMouseEventHandler(titleBar),
gradientStartColor = gradientStartColor,
style = style,
applyTitleBar = { height, _ ->
titleBar.height = height.value
titleBar.putProperty("controls.dark", style.colors.background.isDark())
JBR.getWindowDecorations().setCustomTitleBar(window, titleBar)
PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp)
},
content = content,
)
}
internal fun Modifier.customTitleBarMouseEventHandler(titleBar: CustomTitleBar): Modifier =
pointerInput(Unit) {
val currentContext = currentCoroutineContext()
awaitPointerEventScope {
var inUserControl = false
while (currentContext.isActive) {
val event = awaitPointerEvent(PointerEventPass.Main)
event.changes.forEach {
if (!it.isConsumed && !inUserControl) {
titleBar.forceHitTest(false)
} else {
if (event.type == PointerEventType.Press) {
inUserControl = true
}
if (event.type == PointerEventType.Release) {
inUserControl = false
}
titleBar.forceHitTest(true)
}
}
}
}
}

View File

@@ -0,0 +1,275 @@
package org.jetbrains.jewel.window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.NoInspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import java.awt.Window
import kotlin.math.max
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalContentColor
import org.jetbrains.jewel.foundation.theme.OverrideDarkMode
import org.jetbrains.jewel.ui.component.styling.LocalDefaultDropdownStyle
import org.jetbrains.jewel.ui.component.styling.LocalIconButtonStyle
import org.jetbrains.jewel.ui.util.isDark
import org.jetbrains.jewel.window.styling.TitleBarStyle
import org.jetbrains.jewel.window.utils.DesktopPlatform
import org.jetbrains.jewel.window.utils.macos.MacUtil
internal const val TITLE_BAR_COMPONENT_LAYOUT_ID_PREFIX = "__TITLE_BAR_"
internal const val TITLE_BAR_LAYOUT_ID = "__TITLE_BAR_CONTENT__"
internal const val TITLE_BAR_BORDER_LAYOUT_ID = "__TITLE_BAR_BORDER__"
@Composable
public fun DecoratedWindowScope.TitleBar(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
when (DesktopPlatform.Current) {
DesktopPlatform.Linux -> TitleBarOnLinux(modifier, gradientStartColor, style, content)
DesktopPlatform.Windows -> TitleBarOnWindows(modifier, gradientStartColor, style, content)
DesktopPlatform.MacOS -> TitleBarOnMacOs(modifier, gradientStartColor, style, content)
DesktopPlatform.Unknown -> error("TitleBar is not supported on this platform(${System.getProperty("os.name")})")
}
}
@Composable
internal fun DecoratedWindowScope.TitleBarImpl(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = JewelTheme.defaultTitleBarStyle,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
val titleBarInfo = LocalTitleBarInfo.current
val background by style.colors.backgroundFor(state)
val density = LocalDensity.current
val backgroundBrush =
remember(background, gradientStartColor) {
if (gradientStartColor.isUnspecified) {
SolidColor(background)
} else {
with(density) {
Brush.horizontalGradient(
0.0f to background,
0.5f to gradientStartColor,
1.0f to background,
startX = style.metrics.gradientStartX.toPx(),
endX = style.metrics.gradientEndX.toPx(),
)
}
}
}
Layout(
content = {
CompositionLocalProvider(
LocalContentColor provides style.colors.content,
LocalIconButtonStyle provides style.iconButtonStyle,
LocalDefaultDropdownStyle provides style.dropdownStyle,
) {
OverrideDarkMode(background.isDark()) {
val scope = TitleBarScopeImpl(titleBarInfo.title, titleBarInfo.icon)
scope.content(state)
}
}
},
modifier =
modifier
.background(backgroundBrush)
.focusProperties { canFocus = false }
.layoutId(TITLE_BAR_LAYOUT_ID)
.height(style.metrics.height)
.onSizeChanged { with(density) { applyTitleBar(it.height.toDp(), state) } }
.fillMaxWidth(),
measurePolicy = rememberTitleBarMeasurePolicy(window, state, applyTitleBar),
)
Spacer(Modifier.layoutId(TITLE_BAR_BORDER_LAYOUT_ID).height(1.dp).fillMaxWidth().background(style.colors.border))
}
internal class TitleBarMeasurePolicy(
private val window: Window,
private val state: DecoratedWindowState,
private val applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
) : MeasurePolicy {
override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
if (measurables.isEmpty()) {
return layout(width = constraints.minWidth, height = constraints.minHeight) {}
}
var occupiedSpaceHorizontally = 0
var maxSpaceVertically = constraints.minHeight
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val measuredPlaceable = mutableListOf<Pair<Measurable, Placeable>>()
for (it in measurables) {
val placeable = it.measure(contentConstraints.offset(horizontal = -occupiedSpaceHorizontally))
if (constraints.maxWidth < occupiedSpaceHorizontally + placeable.width) {
break
}
occupiedSpaceHorizontally += placeable.width
maxSpaceVertically = max(maxSpaceVertically, placeable.height)
measuredPlaceable += it to placeable
}
val boxHeight = maxSpaceVertically
val contentPadding = applyTitleBar(boxHeight.toDp(), state)
val leftInset = contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
val rightInset = contentPadding.calculateRightPadding(layoutDirection).roundToPx()
occupiedSpaceHorizontally += leftInset
occupiedSpaceHorizontally += rightInset
val boxWidth = maxOf(constraints.minWidth, occupiedSpaceHorizontally)
return layout(boxWidth, boxHeight) {
if (state.isFullscreen) {
MacUtil.updateFullScreenButtons(window)
}
val placeableGroups =
measuredPlaceable.groupBy { (measurable, _) ->
(measurable.parentData as? TitleBarChildDataNode)?.horizontalAlignment
?: Alignment.CenterHorizontally
}
var headUsedSpace = leftInset
var trailerUsedSpace = rightInset
placeableGroups[Alignment.Start]?.forEach { (_, placeable) ->
val x = headUsedSpace
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
placeable.placeRelative(x, y)
headUsedSpace += placeable.width
}
placeableGroups[Alignment.End]?.forEach { (_, placeable) ->
val x = boxWidth - placeable.width - trailerUsedSpace
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
placeable.placeRelative(x, y)
trailerUsedSpace += placeable.width
}
val centerPlaceable = placeableGroups[Alignment.CenterHorizontally].orEmpty()
val requiredCenterSpace = centerPlaceable.sumOf { it.second.width }
val minX = headUsedSpace
val maxX = boxWidth - trailerUsedSpace - requiredCenterSpace
var centerX = (boxWidth - requiredCenterSpace) / 2
if (minX <= maxX) {
if (centerX > maxX) {
centerX = maxX
}
if (centerX < minX) {
centerX = minX
}
centerPlaceable.forEach { (_, placeable) ->
val x = centerX
val y = Alignment.CenterVertically.align(placeable.height, boxHeight)
placeable.placeRelative(x, y)
centerX += placeable.width
}
}
}
}
}
@Composable
internal fun rememberTitleBarMeasurePolicy(
window: Window,
state: DecoratedWindowState,
applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues,
): MeasurePolicy = remember(window, state, applyTitleBar) { TitleBarMeasurePolicy(window, state, applyTitleBar) }
public interface TitleBarScope {
public val title: String
public val icon: Painter?
@Stable public fun Modifier.align(alignment: Alignment.Horizontal): Modifier
}
private class TitleBarScopeImpl(override val title: String, override val icon: Painter?) : TitleBarScope {
override fun Modifier.align(alignment: Alignment.Horizontal): Modifier =
this then
TitleBarChildDataElement(
alignment,
debugInspectorInfo {
name = "align"
value = alignment
},
)
}
private class TitleBarChildDataElement(
val horizontalAlignment: Alignment.Horizontal,
val inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
) : ModifierNodeElement<TitleBarChildDataNode>(), InspectableValue {
override fun create(): TitleBarChildDataNode = TitleBarChildDataNode(horizontalAlignment)
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? TitleBarChildDataElement ?: return false
return horizontalAlignment == otherModifier.horizontalAlignment
}
override fun hashCode(): Int = horizontalAlignment.hashCode()
override fun update(node: TitleBarChildDataNode) {
node.horizontalAlignment = horizontalAlignment
}
override fun InspectorInfo.inspectableProperties() {
inspectorInfo()
}
}
private class TitleBarChildDataNode(var horizontalAlignment: Alignment.Horizontal) :
ParentDataModifierNode, Modifier.Node() {
override fun Density.modifyParentData(parentData: Any?) = this@TitleBarChildDataNode
}

View File

@@ -0,0 +1,46 @@
package org.jetbrains.jewel.window.styling
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.window.DecoratedWindowState
@Immutable
@GenerateDataFunctions
public class DecoratedWindowStyle(
public val colors: DecoratedWindowColors,
public val metrics: DecoratedWindowMetrics,
) {
public companion object
}
@Immutable
@GenerateDataFunctions
public class DecoratedWindowColors(public val border: Color, public val borderInactive: Color) {
@Composable
public fun borderFor(state: DecoratedWindowState): State<Color> =
rememberUpdatedState(
when {
!state.isActive -> borderInactive
else -> border
}
)
public companion object
}
@Immutable
@GenerateDataFunctions
public class DecoratedWindowMetrics(public val borderWidth: Dp) {
public companion object
}
public val LocalDecoratedWindowStyle: ProvidableCompositionLocal<DecoratedWindowStyle> = staticCompositionLocalOf {
error("No DecoratedWindowStyle provided. Have you forgotten the theme?")
}

View File

@@ -0,0 +1,92 @@
package org.jetbrains.jewel.window.styling
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
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.foundation.GenerateDataFunctions
import org.jetbrains.jewel.ui.component.styling.DropdownStyle
import org.jetbrains.jewel.ui.component.styling.IconButtonStyle
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.window.DecoratedWindowState
@Stable
@GenerateDataFunctions
public class TitleBarStyle(
public val colors: TitleBarColors,
public val metrics: TitleBarMetrics,
public val icons: TitleBarIcons,
public val dropdownStyle: DropdownStyle,
public val iconButtonStyle: IconButtonStyle,
public val paneButtonStyle: IconButtonStyle,
public val paneCloseButtonStyle: IconButtonStyle,
) {
public companion object
}
@Immutable
@GenerateDataFunctions
public class TitleBarColors(
public val background: Color,
public val inactiveBackground: Color,
public val content: Color,
public val border: Color,
// The background color for newControlButtons(three circles in left top corner) in MacOS
// fullscreen mode
public val fullscreenControlButtonsBackground: Color,
// The hover and press background color for window control buttons(minimize, maximize) in Linux
public val titlePaneButtonHoveredBackground: Color,
public val titlePaneButtonPressedBackground: Color,
// The hover and press background color for window close button in Linux
public val titlePaneCloseButtonHoveredBackground: Color,
public val titlePaneCloseButtonPressedBackground: Color,
// The hover and press background color for IconButtons in title bar content
public val iconButtonHoveredBackground: Color,
public val iconButtonPressedBackground: Color,
// The hover and press background color for Dropdown in title bar content
public val dropdownPressedBackground: Color,
public val dropdownHoveredBackground: Color,
) {
@Composable
public fun backgroundFor(state: DecoratedWindowState): State<Color> =
rememberUpdatedState(
when {
!state.isActive -> inactiveBackground
else -> background
}
)
public companion object
}
@Immutable
@GenerateDataFunctions
public class TitleBarMetrics(
public val height: Dp,
public val gradientStartX: Dp,
public val gradientEndX: Dp,
public val titlePaneButtonSize: DpSize,
) {
public companion object
}
@Immutable
@GenerateDataFunctions
public class TitleBarIcons(
public val minimizeButton: IconKey,
public val maximizeButton: IconKey,
public val restoreButton: IconKey,
public val closeButton: IconKey,
) {
public companion object
}
public val LocalTitleBarStyle: ProvidableCompositionLocal<TitleBarStyle> = staticCompositionLocalOf {
error("No TitleBarStyle provided. Have you forgotten the theme?")
}

View File

@@ -0,0 +1,20 @@
package org.jetbrains.jewel.window.utils
public enum class DesktopPlatform {
Linux,
Windows,
MacOS,
Unknown;
public companion object {
public val Current: DesktopPlatform by lazy {
val name = System.getProperty("os.name")
when {
name?.startsWith("Linux") == true -> Linux
name?.startsWith("Win") == true -> Windows
name == "Mac OS X" -> MacOS
else -> Unknown
}
}
}
}

View File

@@ -0,0 +1,42 @@
package org.jetbrains.jewel.window.utils
import com.sun.jna.Native
import java.util.logging.Level
import java.util.logging.Logger
import kotlin.system.measureTimeMillis
internal object JnaLoader {
private var loaded: Boolean? = null
private val logger = Logger.getLogger(JnaLoader::class.java.simpleName)
@Synchronized
fun load() {
if (loaded == null) {
loaded = false
try {
val time = measureTimeMillis { Native.POINTER_SIZE }
logger.info("JNA library (${Native.POINTER_SIZE shl 3}-bit) loaded in $time ms")
loaded = true
} catch (@Suppress("TooGenericExceptionCaught") t: Throwable) {
logger.log(
Level.WARNING,
"Unable to load JNA library(os=${
System.getProperty("os.name")
} ${System.getProperty("os.version")}, jna.boot.library.path=${
System.getProperty("jna.boot.library.path")
})",
t,
)
}
}
}
@get:Synchronized
val isLoaded: Boolean
get() {
if (loaded == null) {
load()
}
return loaded ?: false
}
}

View File

@@ -0,0 +1,67 @@
package org.jetbrains.jewel.window.utils
import java.lang.reflect.AccessibleObject
import java.util.logging.Level
import java.util.logging.Logger
import sun.misc.Unsafe
internal object UnsafeAccessing {
private val logger = Logger.getLogger(UnsafeAccessing::class.java.simpleName)
private val unsafe: Any? by lazy {
try {
val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe")
theUnsafe.isAccessible = true
theUnsafe.get(null) as Unsafe
} catch (@Suppress("TooGenericExceptionCaught") error: Throwable) {
logger.log(Level.WARNING, "Unsafe accessing initializing failed.", error)
null
}
}
val desktopModule by lazy { ModuleLayer.boot().findModule("java.desktop").get() }
val ownerModule: Module by lazy { this.javaClass.module }
private val isAccessibleFieldOffset: Long? by lazy {
try {
(unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first"))
} catch (_: Throwable) {
null
}
}
private val implAddOpens by lazy {
try {
Module::class.java.getDeclaredMethod("implAddOpens", String::class.java, Module::class.java).accessible()
} catch (_: Throwable) {
null
}
}
fun assignAccessibility(obj: AccessibleObject) {
try {
val theUnsafe = unsafe as? Unsafe ?: return
val offset = isAccessibleFieldOffset ?: return
theUnsafe.putBooleanVolatile(obj, offset, true)
} catch (_: Throwable) {
// ignore
}
}
fun assignAccessibility(module: Module, packages: List<String>) {
try {
packages.forEach { implAddOpens?.invoke(module, it, ownerModule) }
} catch (_: Throwable) {
// ignore
}
}
private class Parent {
var first = false
@Volatile var second: Any? = null
}
}
internal fun <T : AccessibleObject> T.accessible(): T = apply { UnsafeAccessing.assignAccessibility(this) }

View File

@@ -0,0 +1,90 @@
package org.jetbrains.jewel.window.utils.macos
import com.sun.jna.Function
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import java.lang.reflect.Proxy
import java.util.Arrays
import java.util.Collections
import java.util.logging.Level
import java.util.logging.Logger
import org.jetbrains.jewel.window.utils.JnaLoader
internal object Foundation {
private val logger = Logger.getLogger(Foundation::class.java.simpleName)
init {
if (!JnaLoader.isLoaded) {
logger.log(Level.WARNING, "JNA is not loaded")
}
}
private val myFoundationLibrary: FoundationLibrary? by lazy {
try {
Native.load("Foundation", FoundationLibrary::class.java, Collections.singletonMap("jna.encoding", "UTF8"))
} catch (_: Throwable) {
null
}
}
private val myObjcMsgSend: Function? by lazy {
try {
(Proxy.getInvocationHandler(myFoundationLibrary) as Library.Handler)
.nativeLibrary
.getFunction("objc_msgSend")
} catch (_: Throwable) {
null
}
}
/** Get the ID of the NSClass with className */
fun getObjcClass(className: String?): ID? = myFoundationLibrary?.objc_getClass(className)
fun getProtocol(name: String?): ID? = myFoundationLibrary?.objc_getProtocol(name)
fun createSelector(s: String?): Pointer? = myFoundationLibrary?.sel_registerName(s)
private fun prepInvoke(id: ID?, selector: Pointer?, args: Array<out Any?>): Array<Any?> {
val invokArgs = arrayOfNulls<Any>(args.size + 2)
invokArgs[0] = id
invokArgs[1] = selector
System.arraycopy(args, 0, invokArgs, 2, args.size)
return invokArgs
}
// objc_msgSend is called with the calling convention of the target method
// on x86_64 this does not make a difference, but arm64 uses a different calling convention for
// varargs
// it is therefore important to not call objc_msgSend as a vararg function
operator fun invoke(id: ID?, selector: Pointer?, vararg args: Any?): ID =
ID(myObjcMsgSend?.invokeLong(prepInvoke(id, selector, args)) ?: 0)
/**
* Invokes the given vararg selector. Expects `NSArray arrayWithObjects:(id), ...` like signature, i.e. exactly one
* fixed argument, followed by varargs.
*/
fun invokeVarArg(id: ID?, selector: Pointer?, vararg args: Any?): ID {
// c functions and objc methods have at least 1 fixed argument, we therefore need to
// separate out the first argument
return myFoundationLibrary?.objc_msgSend(id, selector, args[0], *Arrays.copyOfRange(args, 1, args.size))
?: ID.NIL
}
operator fun invoke(cls: String?, selector: String?, vararg args: Any?): ID =
invoke(getObjcClass(cls), createSelector(selector), *args)
fun invokeVarArg(cls: String?, selector: String?, vararg args: Any?): ID =
invokeVarArg(getObjcClass(cls), createSelector(selector), *args)
fun safeInvoke(stringCls: String?, stringSelector: String?, vararg args: Any?): ID {
val cls = getObjcClass(stringCls)
val selector = createSelector(stringSelector)
if (!invoke(cls, "respondsToSelector:", selector).booleanValue()) {
error("Missing selector $stringSelector for $stringCls")
}
return invoke(cls, selector, *args)
}
operator fun invoke(id: ID?, selector: String?, vararg args: Any?): ID = invoke(id, createSelector(selector), *args)
}

View File

@@ -0,0 +1,122 @@
package org.jetbrains.jewel.window.utils.macos
import com.sun.jna.Callback
import com.sun.jna.Library
import com.sun.jna.Pointer
@Suppress(
"ktlint:standard:function-naming",
"ktlint:standard:property-naming",
"Unused",
"FunctionName",
"ConstPropertyName",
) // Borrowed code
internal interface FoundationLibrary : Library {
fun NSLog(pString: Pointer?, thing: Any?)
fun NSFullUserName(): ID?
fun objc_allocateClassPair(supercls: ID?, name: String?, extraBytes: Int): ID?
fun objc_registerClassPair(cls: ID?)
fun CFStringCreateWithBytes(
allocator: Pointer?,
bytes: ByteArray?,
byteCount: Int,
encoding: Int,
isExternalRepresentation: Byte,
): ID?
fun CFStringGetCString(theString: ID?, buffer: ByteArray?, bufferSize: Int, encoding: Int): Byte
fun CFStringGetLength(theString: ID?): Int
fun CFStringConvertNSStringEncodingToEncoding(nsEncoding: Long): Long
fun CFStringConvertEncodingToIANACharSetName(cfEncoding: Long): ID?
fun CFStringConvertIANACharSetNameToEncoding(encodingName: ID?): Long
fun CFStringConvertEncodingToNSStringEncoding(cfEncoding: Long): Long
fun CFRetain(cfTypeRef: ID?)
fun CFRelease(cfTypeRef: ID?)
fun CFGetRetainCount(cfTypeRef: Pointer?): Int
fun objc_getClass(className: String?): ID?
fun objc_getProtocol(name: String?): ID?
fun class_createInstance(pClass: ID?, extraBytes: Int): ID?
fun sel_registerName(selectorName: String?): Pointer?
fun class_replaceMethod(cls: ID?, selName: Pointer?, impl: Callback?, types: String?): ID?
fun objc_getMetaClass(name: String?): ID?
/**
* Note: Vararg version. Should only be used only for selectors with a single fixed argument followed by varargs.
*/
fun objc_msgSend(receiver: ID?, selector: Pointer?, firstArg: Any?, vararg args: Any?): ID?
fun class_respondsToSelector(cls: ID?, selName: Pointer?): Boolean
fun class_addMethod(cls: ID?, selName: Pointer?, imp: Callback?, types: String?): Boolean
fun class_addMethod(cls: ID?, selName: Pointer?, imp: ID?, types: String?): Boolean
fun class_addProtocol(aClass: ID?, protocol: ID?): Boolean
fun class_isMetaClass(cls: ID?): Boolean
fun NSStringFromSelector(selector: Pointer?): ID?
fun NSStringFromClass(aClass: ID?): ID?
fun objc_getClass(clazz: Pointer?): Pointer?
companion object {
const val kCFStringEncodingMacRoman = 0
const val kCFStringEncodingWindowsLatin1 = 0x0500
const val kCFStringEncodingISOLatin1 = 0x0201
const val kCFStringEncodingNextStepLatin = 0x0B01
const val kCFStringEncodingASCII = 0x0600
const val kCFStringEncodingUnicode = 0x0100
const val kCFStringEncodingUTF8 = 0x08000100
const val kCFStringEncodingNonLossyASCII = 0x0BFF
const val kCFStringEncodingUTF16 = 0x0100
const val kCFStringEncodingUTF16BE = 0x10000100
const val kCFStringEncodingUTF16LE = 0x14000100
const val kCFStringEncodingUTF32 = 0x0c000100
const val kCFStringEncodingUTF32BE = 0x18000100
const val kCFStringEncodingUTF32LE = 0x1c000100
// https://developer.apple.com/library/mac/documentation/Carbon/Reference/CGWindow_Reference/Constants/Constants.html#//apple_ref/doc/constant_group/Window_List_Option_Constants
const val kCGWindowListOptionAll = 0
const val kCGWindowListOptionOnScreenOnly = 1
const val kCGWindowListOptionOnScreenAboveWindow = 2
const val kCGWindowListOptionOnScreenBelowWindow = 4
const val kCGWindowListOptionIncludingWindow = 8
const val kCGWindowListExcludeDesktopElements = 16
// https://developer.apple.com/library/mac/documentation/Carbon/Reference/CGWindow_Reference/Constants/Constants.html#//apple_ref/doc/constant_group/Window_Image_Types
const val kCGWindowImageDefault = 0
const val kCGWindowImageBoundsIgnoreFraming = 1
const val kCGWindowImageShouldBeOpaque = 2
const val kCGWindowImageOnlyShadows = 4
const val kCGWindowImageBestResolution = 8
const val kCGWindowImageNominalResolution = 16
// see enum NSBitmapImageFileType
const val NSBitmapImageFileTypeTIFF = 0
const val NSBitmapImageFileTypeBMP = 1
const val NSBitmapImageFileTypeGIF = 2
const val NSBitmapImageFileTypeJPEG = 3
const val NSBitmapImageFileTypePNG = 4
const val NSBitmapImageFileTypeJPEG2000 = 5
}
}

View File

@@ -0,0 +1,26 @@
package org.jetbrains.jewel.window.utils.macos
import com.sun.jna.NativeLong
/** Could be an address in memory (if pointer to a class or method) or a value (like 0 or 1) */
@Suppress("OVERRIDE_DEPRECATION") // Copied code
internal class ID : NativeLong {
constructor()
constructor(peer: Long) : super(peer)
fun booleanValue(): Boolean = toInt() != 0
override fun toByte(): Byte = toInt().toByte()
override fun toChar(): Char = toInt().toChar()
override fun toShort(): Short = toInt().toShort()
@Suppress("RedundantOverride") // Without this, we get a SOE
override fun toInt(): Int = super.toInt()
companion object {
@JvmField val NIL = ID(0L)
}
}

View File

@@ -0,0 +1,95 @@
package org.jetbrains.jewel.window.utils.macos
import java.awt.Component
import java.awt.Window
import java.lang.reflect.InvocationTargetException
import java.util.logging.Level
import java.util.logging.Logger
import javax.swing.SwingUtilities
import org.jetbrains.jewel.window.utils.UnsafeAccessing
import org.jetbrains.jewel.window.utils.accessible
internal object MacUtil {
private val logger = Logger.getLogger(MacUtil::class.java.simpleName)
init {
try {
UnsafeAccessing.assignAccessibility(
UnsafeAccessing.desktopModule,
listOf("sun.awt", "sun.lwawt", "sun.lwawt.macosx"),
)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
logger.log(Level.WARNING, "Assign access for jdk.desktop failed.", e)
}
}
fun getWindowFromJavaWindow(w: Window?): ID {
if (w == null) {
return ID.NIL
}
try {
val cPlatformWindow = getPlatformWindow(w)
if (cPlatformWindow != null) {
val ptr = cPlatformWindow.javaClass.superclass.getDeclaredField("ptr")
ptr.setAccessible(true)
return ID(ptr.getLong(cPlatformWindow))
}
} catch (e: IllegalAccessException) {
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
} catch (e: NoSuchFieldException) {
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
}
return ID.NIL
}
fun getPlatformWindow(w: Window): Any? {
try {
val awtAccessor = Class.forName("sun.awt.AWTAccessor")
val componentAccessor = awtAccessor.getMethod("getComponentAccessor").invoke(null)
val getPeer = componentAccessor.javaClass.getMethod("getPeer", Component::class.java).accessible()
val peer = getPeer.invoke(componentAccessor, w)
if (peer != null) {
val cWindowPeerClass: Class<*> = peer.javaClass
val getPlatformWindowMethod = cWindowPeerClass.getDeclaredMethod("getPlatformWindow")
val cPlatformWindow = getPlatformWindowMethod.invoke(peer)
if (cPlatformWindow != null) {
return cPlatformWindow
}
}
} catch (e: NoSuchMethodException) {
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
} catch (e: IllegalAccessException) {
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
} catch (e: InvocationTargetException) {
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
} catch (e: ClassNotFoundException) {
logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e)
}
return null
}
fun updateColors(w: Window) {
SwingUtilities.invokeLater {
val window = getWindowFromJavaWindow(w)
val delegate = Foundation.invoke(window, "delegate")
if (
Foundation.invoke(delegate, "respondsToSelector:", Foundation.createSelector("updateColors"))
.booleanValue()
) {
Foundation.invoke(delegate, "updateColors")
}
}
}
fun updateFullScreenButtons(w: Window) {
SwingUtilities.invokeLater {
val selector = Foundation.createSelector("updateFullScreenButtons")
val window = getWindowFromJavaWindow(w)
val delegate = Foundation.invoke(window, "delegate")
if (Foundation.invoke(delegate, "respondsToSelector:", selector).booleanValue()) {
Foundation.invoke(delegate, "updateFullScreenButtons")
}
}
}
}

189
platform/jewel/detekt.yml Normal file
View File

@@ -0,0 +1,189 @@
build:
maxIssues: 0
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ''
processors:
active: true
exclude:
- 'DetektProgressListener'
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ClassCountProcessor'
# - 'PackageCountProcessor'
# - 'KtFileCountProcessor'
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
# - 'FindingsReport'
- 'FileBasedFindingsReport'
comments:
active: false
complexity:
active: true
LongParameterList:
active: false
TooManyFunctions:
active: false
LongMethod:
active: false
CyclomaticComplexMethod:
active: false
coroutines:
active: true
GlobalCoroutineUsage:
active: true
RedundantSuspendModifier:
active: true
SleepInsteadOfDelay:
active: true
SuspendFunWithFlowReturnType:
active: true
exceptions:
active: true
NotImplementedDeclaration:
active: true
ObjectExtendsThrowable:
active: true
performance:
active: true
SpreadOperator:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ]
potential-bugs:
active: true
DontDowncastCollectionTypes:
active: true
ExitOutsideMain:
active: true
HasPlatformType:
active: true
IgnoredReturnValue:
active: true
ImplicitUnitReturnType:
active: true
allowExplicitReturnType: true
MapGetWithNotNullAssertionOperator:
active: true
UnconditionalJumpStatementInLoop:
active: true
UnreachableCatchBlock:
active: true
UselessPostfixExpression:
active: true
style:
active: true
CollapsibleIfStatements:
active: true
DataClassShouldBeImmutable:
active: true
EqualsOnSignatureLine:
active: true
ExpressionBodySyntax:
active: true
includeLineWrapping: false
ForbiddenComment:
active: true
comments:
- value: 'STOPSHIP'
reason: 'Forbidden STOPSHIP marker in comment, please address before shipping'
allowedPatterns: ''
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 3
MagicNumber:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ]
ignoreNumbers: [ '-1', '0', '1', '2' ]
ignoreHashCodeFunction: true
ignorePropertyDeclaration: true
ignoreLocalVariableDeclaration: true
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: true
ignoreNamedArgument: true
ignoreEnums: true
ignoreRanges: true
MandatoryBracesLoops:
active: true
MaxLineLength:
active: true
maxLineLength: 150
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
NoTabs:
active: true
OptionalUnit:
active: true
PreferToOverPairSyntax:
active: true
RedundantExplicitType:
active: true
ReturnCount:
active: false
SpacingBetweenPackageAndImports:
active: true
ThrowsCount:
active: true
max: 5
TrailingWhitespace:
active: true
UnnecessaryLet:
active: true
UnnecessaryParentheses:
active: false
UnnecessaryAbstractClass:
ignoreAnnotated:
- Module
UntilInsteadOfRangeTo:
active: true
UnusedImports:
active: true
UnusedPrivateMember:
# We need to disable this otherwise we'd need to @Suppress all Composable previews...
active: false
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
active: true
UseCheckOrError:
active: true
UseEmptyCounterpart:
active: true
UseIfEmptyOrIfBlank:
active: true
UseIsNullOrEmpty:
active: true
UseRequire:
active: true
UseRequireNotNull:
active: true
VarCouldBeVal:
active: true
naming:
FunctionNaming:
active: false
ignoreAnnotated:
- Composable

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
import org.jetbrains.compose.ComposeBuildConfig
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
alias(libs.plugins.compose.compiler)
}
private val composeVersion
get() = ComposeBuildConfig.composeVersion
dependencies {
api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")
testImplementation(compose.desktop.uiTestJUnit4)
testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") }
}

View File

@@ -0,0 +1,14 @@
package org.jetbrains.jewel.foundation
/**
* Enables the new compositing strategy for rendering directly into Swing Graphics. This fixes z-order problems and
* artifacts on resizing, but has a performance penalty when using infinitely repeating animations.
*
* We assume the majority of our users will want this flag to be on, so this convenience function is provided to that
* end. Make sure you call it **before** you initialize any Compose content. The function is idempotent and extremely
* cheap, so you can call it on any entry point.
*/
@ExperimentalJewelApi
public fun enableNewSwingCompositing() {
System.setProperty("compose.swing.render.on.graphics", "true")
}

View File

@@ -0,0 +1,20 @@
package org.jetbrains.jewel.foundation
import kotlin.RequiresOptIn.Level
@RequiresOptIn(
level = Level.WARNING,
message = "This is an experimental API for Jewel and is likely to change before becoming stable.",
)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.FIELD,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.VALUE_PARAMETER,
)
public annotation class ExperimentalJewelApi

View File

@@ -0,0 +1,9 @@
package org.jetbrains.jewel.foundation
/**
* Instructs the Poko compiler plugin to generate equals, hashcode, and toString functions for the class it's attached
* to.
*
* See [this issue](https://github.com/JetBrains/jewel/issues/83) for details.
*/
@Target(AnnotationTarget.CLASS) public annotation class GenerateDataFunctions

View File

@@ -0,0 +1,51 @@
package org.jetbrains.jewel.foundation
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
@Immutable
@GenerateDataFunctions
public class GlobalColors(
public val borders: BorderColors,
public val outlines: OutlineColors,
public val text: TextColors,
public val panelBackground: Color,
) {
public companion object
}
@Immutable
@GenerateDataFunctions
public class TextColors(
public val normal: Color,
public val selected: Color,
public val disabled: Color,
public val info: Color,
public val error: Color,
) {
public companion object
}
@Immutable
@GenerateDataFunctions
public class BorderColors(public val normal: Color, public val focused: Color, public val disabled: Color) {
public companion object
}
@Immutable
@GenerateDataFunctions
public class OutlineColors(
public val focused: Color,
public val focusedWarning: Color,
public val focusedError: Color,
public val warning: Color,
public val error: Color,
) {
public companion object
}
public val LocalGlobalColors: ProvidableCompositionLocal<GlobalColors> = staticCompositionLocalOf {
error("No GlobalColors provided. Have you forgotten the theme?")
}

View File

@@ -0,0 +1,16 @@
package org.jetbrains.jewel.foundation
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
@Immutable
@GenerateDataFunctions
public class GlobalMetrics(public val outlineWidth: Dp, public val rowHeight: Dp) {
public companion object
}
public val LocalGlobalMetrics: ProvidableCompositionLocal<GlobalMetrics> = staticCompositionLocalOf {
error("No GlobalMetrics provided. Have you forgotten the theme?")
}

View File

@@ -0,0 +1,20 @@
package org.jetbrains.jewel.foundation
import kotlin.RequiresOptIn.Level
@RequiresOptIn(
level = Level.WARNING,
message = "This is an internal API for Jewel and is subject to change without notice.",
)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.FIELD,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.TYPEALIAS,
AnnotationTarget.VALUE_PARAMETER,
)
public annotation class InternalJewelApi

View File

@@ -0,0 +1,34 @@
package org.jetbrains.jewel.foundation
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.RoundRect
internal fun RoundRect.grow(delta: Float) =
RoundRect(
left = left - delta,
top = top - delta,
right = right + delta,
bottom = bottom + delta,
topLeftCornerRadius = CornerRadius(topLeftCornerRadius.x + delta, topLeftCornerRadius.y + delta),
topRightCornerRadius = CornerRadius(topRightCornerRadius.x + delta, topRightCornerRadius.y + delta),
bottomLeftCornerRadius = CornerRadius(bottomLeftCornerRadius.x + delta, bottomLeftCornerRadius.y + delta),
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x + delta, bottomRightCornerRadius.y + delta),
)
internal fun RoundRect.shrink(delta: Float) =
RoundRect(
left = left + delta,
top = top + delta,
right = right - delta,
bottom = bottom - delta,
topLeftCornerRadius = CornerRadius(topLeftCornerRadius.x - delta, topLeftCornerRadius.y - delta),
topRightCornerRadius = CornerRadius(topRightCornerRadius.x - delta, topRightCornerRadius.y - delta),
bottomLeftCornerRadius = CornerRadius(bottomLeftCornerRadius.x - delta, bottomLeftCornerRadius.y - delta),
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x - delta, bottomRightCornerRadius.y - delta),
)
internal fun RoundRect.hasAtLeastOneNonRoundedCorner() =
topLeftCornerRadius.x == 0f && topLeftCornerRadius.y == 0f ||
topRightCornerRadius.x == 0f && topRightCornerRadius.y == 0f ||
bottomLeftCornerRadius.x == 0f && bottomLeftCornerRadius.y == 0f ||
bottomRightCornerRadius.x == 0f && bottomRightCornerRadius.y == 0f

View File

@@ -0,0 +1,62 @@
package org.jetbrains.jewel.foundation
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.unit.Dp
public sealed class Stroke {
@Immutable
public object None : Stroke() {
override fun toString(): String = "None"
}
@Immutable
@GenerateDataFunctions
public class Solid
internal constructor(
public val width: Dp,
public val color: Color,
public val alignment: Alignment,
public val expand: Dp,
) : Stroke()
@Immutable
@GenerateDataFunctions
public class Brush
internal constructor(
public val width: Dp,
public val brush: androidx.compose.ui.graphics.Brush,
public val alignment: Alignment,
public val expand: Dp,
) : Stroke()
public enum class Alignment {
Inside,
Center,
Outside,
}
}
public fun Stroke(width: Dp, color: Color, alignment: Stroke.Alignment, expand: Dp = Dp.Unspecified): Stroke {
if (width.value == 0f) return Stroke.None
if (color.isUnspecified) return Stroke.None
return Stroke.Solid(width, color, alignment, expand)
}
public fun Stroke(width: Dp, brush: Brush, alignment: Stroke.Alignment, expand: Dp = Dp.Unspecified): Stroke {
if (width.value == 0f) return Stroke.None
return when (brush) {
is SolidColor -> {
if (brush.value.isUnspecified) {
Stroke.None
} else {
Stroke.Solid(width, brush.value, alignment, expand)
}
}
else -> Stroke.Brush(width, brush, alignment, expand)
}
}

View File

@@ -0,0 +1,7 @@
package org.jetbrains.jewel.foundation.actionSystem
public interface DataProviderContext {
public fun <TValue : Any> set(key: String, value: TValue?)
public fun <TValue : Any> lazy(key: String, initializer: () -> TValue?)
}

View File

@@ -0,0 +1,23 @@
package org.jetbrains.jewel.foundation.actionSystem
import androidx.compose.ui.node.ModifierNodeElement
internal class DataProviderElement(val dataProvider: DataProviderContext.() -> Unit) :
ModifierNodeElement<DataProviderNode>() {
override fun create(): DataProviderNode = DataProviderNode(dataProvider)
override fun update(node: DataProviderNode) {
node.dataProvider = dataProvider
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DataProviderElement
return dataProvider == other.dataProvider
}
override fun hashCode(): Int = dataProvider.hashCode()
}

View File

@@ -0,0 +1,19 @@
package org.jetbrains.jewel.foundation.actionSystem
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusEventModifierNode
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.node.TraversableNode
public class DataProviderNode(public var dataProvider: DataProviderContext.() -> Unit) :
Modifier.Node(), FocusEventModifierNode, TraversableNode {
public var hasFocus: Boolean = false
override fun onFocusEvent(focusState: FocusState) {
hasFocus = focusState.hasFocus
}
override val traverseKey: TraverseKey = TraverseKey
public companion object TraverseKey
}

View File

@@ -0,0 +1,20 @@
package org.jetbrains.jewel.foundation.actionSystem
import androidx.compose.foundation.focusable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusEventModifierNode
/**
* Configure component to provide data for IntelliJ Actions system.
*
* Use this modifier to provide context related data that can be used by IntelliJ Actions functionality such as Search
* Everywhere, Action Popups etc.
*
* Important note: modifiers order is important, so be careful with order of [focusable] and [provideData] (see
* [FocusEventModifierNode]).
*
* This can be traversed from Modifier.Node() using Compose traversal API using DataProviderNode as a TraverseKey
*/
@Suppress("unused")
public fun Modifier.provideData(dataProvider: DataProviderContext.() -> Unit): Modifier =
this then DataProviderElement(dataProvider)

View File

@@ -0,0 +1,389 @@
package org.jetbrains.jewel.foundation.code
import org.jetbrains.jewel.foundation.code.MimeType.Known.AGSL
import org.jetbrains.jewel.foundation.code.MimeType.Known.DART
import org.jetbrains.jewel.foundation.code.MimeType.Known.JSON
import org.jetbrains.jewel.foundation.code.MimeType.Known.KOTLIN
import org.jetbrains.jewel.foundation.code.MimeType.Known.PYTHON
import org.jetbrains.jewel.foundation.code.MimeType.Known.RUST
import org.jetbrains.jewel.foundation.code.MimeType.Known.TYPESCRIPT
import org.jetbrains.jewel.foundation.code.MimeType.Known.YAML
/**
* Represents the language and dialect of a source snippet, as an RFC 2046 mime type.
*
* For example, a Kotlin source file may have the mime type `text/kotlin`. However, if it corresponds to a
* `build.gradle.kts` file, we'll also attach the mime parameter `role=gradle`, resulting in mime type `text/kotlin;
* role=gradle`.
*
* For XML resource files, we'll attach other attributes; for example `role=manifest` for Android manifest files and
* `role=resource` for XML resource files. For the latter we may also attach for example `folderType=values`, and for
* XML files in general, the root tag, such as `text/xml; role=resource; folderType=layout; rootTag=LinearLayout`.
*
* This class does not implement *all* aspects of the RFC; in particular, we don't treat attributes as case-insensitive,
* and we only support value tokens, not value strings -- neither of these are needed for our purposes.
*
* This is implemented using a value class, such that behind the scenes we're really just passing a String around. This
* also means we can't initialize related values such as the corresponding Markdown fenced block names, or IntelliJ
* language id's. Instead, these are looked up via `when`-tables. When adding a new language, update all lookup methods:
* * [displayName]
*/
@JvmInline
public value class MimeType(private val mimeType: String) {
public fun displayName(): String =
when (normalizeString()) {
Known.KOTLIN.mimeType -> if (isGradle()) "Gradle DSL" else "Kotlin"
Known.JAVA.mimeType -> "Java"
Known.XML.mimeType -> {
when (getRole()) {
null -> "XML"
VALUE_MANIFEST -> "Manifest"
VALUE_RESOURCE -> {
val folderType = getAttribute(ATTR_FOLDER_TYPE)
folderType?.capitalizeAsciiOnly() ?: "XML"
}
else -> "XML"
}
}
Known.JSON.mimeType -> "JSON"
Known.TEXT.mimeType -> "Text"
Known.REGEX.mimeType -> "Regular Expression"
Known.GROOVY.mimeType -> if (isGradle()) "Gradle" else "Groovy"
Known.TOML.mimeType -> if (isVersionCatalog()) "Version Catalog" else "TOML"
Known.C.mimeType -> "C"
Known.CPP.mimeType -> "C++"
Known.SVG.mimeType -> "SVG"
Known.AIDL.mimeType -> "AIDL"
Known.SQL.mimeType -> "SQL"
Known.PROGUARD.mimeType -> "Shrinker Config"
Known.PROPERTIES.mimeType -> "Properties"
Known.PROTO.mimeType -> "Protobuf"
Known.PYTHON.mimeType -> "Python"
Known.DART.mimeType -> "Dart"
Known.RUST.mimeType -> "Rust"
Known.JAVASCRIPT.mimeType -> "JavaScript"
Known.AGSL.mimeType -> "Android Graphics Shading Language"
Known.SHELL.mimeType -> "Shell Script"
Known.YAML.mimeType -> "YAML"
Known.GO.mimeType -> "Go"
else -> mimeType
}
private fun normalizeString(): String {
when (this) {
// Built-ins are already normalized, don't do string and sorting work
Known.KOTLIN,
Known.JAVA,
Known.TEXT,
Known.XML,
Known.PROPERTIES,
Known.TOML,
Known.JSON,
Known.REGEX,
Known.GROOVY,
Known.C,
Known.CPP,
Known.SVG,
Known.AIDL,
Known.PROTO,
Known.SQL,
Known.PROGUARD,
Known.MANIFEST,
Known.RESOURCE,
Known.GRADLE,
Known.GRADLE_KTS,
Known.VERSION_CATALOG,
Known.PYTHON,
Known.DART,
Known.RUST,
Known.JAVASCRIPT,
Known.TYPESCRIPT,
Known.AGSL,
Known.SHELL,
Known.YAML,
Known.GO,
Known.UNKNOWN -> return this.mimeType
}
val baseEnd = mimeType.indexOf(';')
val normalizedBase =
when (val base = if (baseEnd == -1) mimeType else mimeType.substring(0, baseEnd)) {
"text/x-java-source",
"application/x-java",
"text/x-java" -> Known.JAVA.mimeType
"application/kotlin-source",
"text/x-kotlin",
"text/x-kotlin-source" -> KOTLIN.mimeType
"application/xml" -> Known.XML.mimeType
"application/json",
"application/vnd.api+json",
"application/hal+json",
"application/ld+json" -> JSON.mimeType
"image/svg+xml" -> Known.XML.mimeType
"text/x-python",
"application/x-python-script" -> PYTHON.mimeType
"text/dart",
"text/x-dart",
"application/dart",
"application/x-dart" -> DART.mimeType
"application/javascript",
"application/x-javascript",
"text/ecmascript",
"application/ecmascript",
"application/x-ecmascript" -> Known.JAVASCRIPT.mimeType
"application/typescript" + "application/x-typescript" -> TYPESCRIPT.mimeType
"text/x-rust",
"application/x-rust" -> RUST.mimeType
"text/x-sksl" -> AGSL.mimeType
"application/yaml",
"text/x-yaml",
"application/x-yaml" -> YAML.mimeType
else -> base
}
if (baseEnd == -1) {
return normalizedBase
}
val attributes =
mimeType
.split(';')
.asSequence()
.drop(1)
.sorted()
.mapNotNull {
val index = it.indexOf('=')
if (index != -1) {
it.substring(0, index).trim() to it.substring(index + 1).trim()
} else {
null
}
}
.filter { isRelevantAttribute(it.first) }
.map { "${it.first}=${it.second}" }
.joinToString("; ")
return if (attributes.isNotBlank()) {
"$normalizedBase; $attributes"
} else {
normalizedBase
}
}
/** Returns whether the given attribute should be included in a normalized string */
private fun isRelevantAttribute(attribute: String): Boolean =
when (attribute) {
ATTR_ROLE,
ATTR_ROOT_TAG,
ATTR_FOLDER_TYPE -> true
else -> false
}
/**
* Returns just the language portion of the mime type.
*
* For example, for `text/kotlin; role=gradle` this will return `text/kotlin`. For `text/plain; charset=us-ascii`
* this returns `text/plain`
*/
public fun base(): MimeType = MimeType(mimeType.substringBefore(';').trim())
internal fun getRole(): String? = getAttribute(ATTR_ROLE)
private fun getFolderType(): String? = getAttribute(ATTR_FOLDER_TYPE)
private fun getAttribute(name: String): String? {
val marker = "$name="
var start = mimeType.indexOf(marker)
if (start == -1) {
return null
}
start += marker.length
var end = start
while (end < mimeType.length && !mimeType[end].isWhitespace()) {
end++
}
return mimeType.substring(start, end).removeSurrounding("\"")
}
override fun toString(): String = mimeType
private companion object {
/**
* Attribute used to indicate the role this source file plays; for example, an XML file may be a "manifest" or a
* "resource".
*/
const val ATTR_ROLE: String = "role"
/** For XML resource files, the folder type if any (such as "values" or "layout") */
const val ATTR_FOLDER_TYPE: String = "folderType"
/** For XML files, the root tag in the content */
const val ATTR_ROOT_TAG: String = "rootTag"
const val VALUE_RESOURCE = "resource"
const val VALUE_MANIFEST = "manifest"
}
public object Known {
// Well known mime types for major languages.
/**
* Well known name for Kotlin source snippets. This is the base mime type; consider using [isKotlin] instead to
* check if a mime type represents Kotlin code such that it also picks up `build.gradle.kts` files (which carry
* extra attributes in the mime type; see [GRADLE_KTS].)
*/
public val KOTLIN: MimeType = MimeType("text/kotlin")
/** Well known name for Java source snippets. */
public val JAVA: MimeType = MimeType("text/java")
/** Well known mime type for text files. */
public val TEXT: MimeType = MimeType("text/plain")
/**
* Special marker mimetype for unknown or unspecified mime types. These will generally be treated as [TEXT] for
* editor purposes. (The standard "unknown" mime type is application/octet-stream (from RFC 2046) but we know
* this isn't binary data; it's text.)
*
* Note that [MimeType] is generally nullable in places where it's optional instead of being set to this value,
* but this mime type is there for places where we need a specific value to point to.
*/
public val UNKNOWN: MimeType = MimeType("text/unknown")
/**
* Well known name for XML source snippets. This is the base mime type; consider using [isXml] instead to check
* if a mime type represents any XML such that it also picks up manifest files, resource files etc., which all
* carry extra attributes in the mime type; see for example [MANIFEST] and [RESOURCE].
*/
public val XML: MimeType = MimeType("text/xml")
public val PROPERTIES: MimeType = MimeType("text/properties")
public val TOML: MimeType = MimeType("text/toml")
public val JSON: MimeType = MimeType("text/json")
public val REGEX: MimeType = MimeType("text/x-regex-source")
public val GROOVY: MimeType = MimeType("text/groovy")
public val C: MimeType = MimeType("text/c")
public val CPP: MimeType = MimeType("text/c++")
public val SVG: MimeType = MimeType("image/svg+xml")
public val AIDL: MimeType = MimeType("text/x-aidl-source")
public val PROTO: MimeType = MimeType("text/x-protobuf")
public val SQL: MimeType = MimeType("text/x-sql")
public val PROGUARD: MimeType = MimeType("text/x-proguard")
public val PYTHON: MimeType = MimeType("text/python")
public val JAVASCRIPT: MimeType = MimeType("text/javascript")
public val TYPESCRIPT: MimeType = MimeType("text/typescript")
public val DART: MimeType = MimeType("application/dart")
public val RUST: MimeType = MimeType("text/rust")
public val AGSL: MimeType = MimeType("text/x-agsl")
public val SHELL: MimeType = MimeType("application/x-sh")
public val YAML: MimeType = MimeType("text/yaml")
public val GO: MimeType = MimeType("text/go")
/** Note that most resource files will also have a folder type, so don't use equality on this mime type */
public val RESOURCE: MimeType = MimeType("$XML; $ATTR_ROLE=resource")
public val MANIFEST: MimeType = MimeType("$XML;$ATTR_ROLE=manifest $ATTR_ROOT_TAG=manifest")
public val GRADLE: MimeType = MimeType("$GROOVY; $ATTR_ROLE=gradle")
public val GRADLE_KTS: MimeType = MimeType("$KOTLIN; $ATTR_ROLE=gradle")
public val VERSION_CATALOG: MimeType = MimeType("$TOML; $ATTR_ROLE=version-catalog")
/** Maps from a markdown language [name] back to a mime type. */
public fun fromMarkdownLanguageName(name: String): MimeType? =
when (name) {
"kotlin",
"kt",
"kts" -> KOTLIN
"java" -> JAVA
"xml" -> XML
"json",
"json5" -> JSON
"regex",
"regexp" -> REGEX
"groovy" -> GROOVY
"toml" -> TOML
"c" -> C
"c++" -> CPP
"svg" -> SVG
"aidl" -> AIDL
"sql" -> SQL
"properties" -> PROPERTIES
"protobuf" -> PROTO
"python2",
"python3",
"py",
"python" -> PYTHON
"dart" -> DART
"rust" -> RUST
"js",
"javascript" -> JAVASCRIPT
"typescript" -> TYPESCRIPT
"sksl" -> AGSL
"sh",
"bash",
"zsh",
"shell" -> SHELL
"yaml",
"yml" -> YAML
"go",
"golang" -> YAML
else -> null
}
}
}
/** Is the base language for this mime type Kotlin? */
public fun MimeType?.isKotlin(): Boolean = this?.base() == MimeType.Known.KOTLIN
/** Is the base language for this mime type Java? */
public fun MimeType?.isJava(): Boolean = this?.base() == MimeType.Known.JAVA
/** Is the base language for this mime type XML? */
public fun MimeType?.isXml(): Boolean = this?.base() == MimeType.Known.XML
/** Is this a Gradle file (which could be in Groovy, *or*, Kotlin) */
public fun MimeType?.isGradle(): Boolean = this?.getRole() == "gradle"
/** Is this a version catalog file (which could be in TOML, or in Groovy) */
public fun MimeType?.isVersionCatalog(): Boolean = this?.getRole() == "version-catalog"
/** Is this an Android manifest file? */
public fun MimeType?.isManifest(): Boolean = this?.getRole() == "manifest"
/** Is the base language for this mime type SQL? */
public fun MimeType?.isSql(): Boolean = this?.base() == MimeType.Known.SQL
/** Is the base language for this mime type a regular expression? */
public fun MimeType?.isRegex(): Boolean = this?.base() == MimeType.Known.REGEX
/** Is the base language for this mime type a protobuf? */
public fun MimeType?.isProto(): Boolean = this?.base() == MimeType.Known.PROTO
private fun String.capitalizeAsciiOnly(): String {
if (isEmpty()) return this
val c = this[0]
return if (c in 'a'..'z') {
buildString(length) {
append(c.uppercaseChar())
append(this@capitalizeAsciiOnly, 1, this@capitalizeAsciiOnly.length)
}
} else {
this
}
}

View File

@@ -0,0 +1,32 @@
package org.jetbrains.jewel.foundation.code.highlighting
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.AnnotatedString
import kotlinx.coroutines.flow.Flow
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.code.MimeType
@ExperimentalJewelApi
public interface CodeHighlighter {
/**
* Highlights [code] according to rules for the language specified by [mimeType], and returns flow of styled
* strings. For basic highlighters with rigid color schemes it is enough to return a flow of one element:
* ```
* return flowOf(highlightedString(code, mimeType))
* ```
*
* However, some implementations might want gradual highlighting (for example, apply something simple while waiting
* for the extensive info from server), or they might rely upon a color scheme that can change at any time.
*
* In such cases, they need to produce more than one styled string for the same piece of code, and that's when flows
* come in handy.
*
* @see [NoOpCodeHighlighter]
*/
public fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString>
}
public val LocalCodeHighlighter: ProvidableCompositionLocal<CodeHighlighter> = staticCompositionLocalOf {
NoOpCodeHighlighter
}

View File

@@ -0,0 +1,10 @@
package org.jetbrains.jewel.foundation.code.highlighting
import androidx.compose.ui.text.AnnotatedString
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.jetbrains.jewel.foundation.code.MimeType
public object NoOpCodeHighlighter : CodeHighlighter {
override fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString> = flowOf(AnnotatedString(code))
}

Some files were not shown because too many files have changed in this diff Show More