├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── classic ├── README.md ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── kanro │ └── compose │ └── jetbrains │ ├── JBTheme.kt │ ├── JBTypography.kt │ ├── color │ ├── ButtonColors.kt │ ├── CheckBoxColors.kt │ ├── FieldColors.kt │ ├── FocusColors.kt │ ├── IconColors.kt │ ├── PanelColors.kt │ ├── ProgressColors.kt │ ├── ScrollColors.kt │ ├── SelectionColors.kt │ ├── TabColors.kt │ ├── TableColors.kt │ ├── TextColors.kt │ ├── ToggleColors.kt │ └── ToolBarColors.kt │ ├── control │ ├── ActionButton.kt │ ├── Button.kt │ ├── CheckBox.kt │ ├── ComboBox.kt │ ├── ContentAlpha.kt │ ├── ContentColor.kt │ ├── ContextMenu.kt │ ├── DropdownMenu.kt │ ├── EmbeddedTextField.kt │ ├── Icon.kt │ ├── JBToolBar.kt │ ├── JBTree.kt │ ├── JPanel.kt │ ├── ListView.kt │ ├── ProgressBar.kt │ ├── Selection.kt │ ├── Tab.kt │ ├── Text.kt │ └── TextField.kt │ └── icons │ ├── __JBIcons.kt │ └── jbicons │ ├── __Actions.kt │ ├── __General.kt │ ├── actions │ ├── ArrowExpand.kt │ ├── ArrowExpandDark.kt │ ├── Checkmark.kt │ ├── CheckmarkIndeterminate.kt │ └── Close.kt │ └── general │ ├── ButtonDropTriangle.kt │ └── ButtonDropTriangleDark.kt ├── docs ├── gradle-jvm.png ├── project-sdk.png ├── screenshot-expui.png └── screenshot.png ├── expui-gallery ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── Main.kt │ └── resources │ └── icons │ ├── darkTheme.svg │ ├── darkTheme_dark.svg │ ├── generic.svg │ ├── generic_dark.svg │ ├── github.svg │ ├── github_dark.svg │ ├── kotlin.svg │ ├── lightTheme.svg │ ├── lightTheme_dark.svg │ ├── settings.svg │ ├── settings_dark.svg │ ├── text.svg │ └── text_dark.svg ├── expui ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── io │ │ └── kanro │ │ └── compose │ │ └── jetbrains │ │ └── expui │ │ ├── DesktopPlatform.kt │ │ ├── control │ │ ├── ActionButton.kt │ │ ├── Button.kt │ │ ├── CheckBox.kt │ │ ├── ComboBox.kt │ │ ├── ContextCompositionLocals.kt │ │ ├── ContextMenu.kt │ │ ├── DropdownMenu.kt │ │ ├── Icon.kt │ │ ├── Indication.kt │ │ ├── Label.kt │ │ ├── Link.kt │ │ ├── Painter.kt │ │ ├── PointerInput.kt │ │ ├── ProgressBar.kt │ │ ├── RadioButton.kt │ │ ├── SegmentedButton.kt │ │ ├── Tab.kt │ │ ├── TextArea.kt │ │ ├── TextField.kt │ │ ├── ToolBar.kt │ │ └── Tooltip.kt │ │ ├── style │ │ ├── AreaColors.kt │ │ ├── AreaProvider.kt │ │ ├── Border.kt │ │ └── TextStyle.kt │ │ ├── theme │ │ ├── DarkTheme.kt │ │ ├── Fonts.kt │ │ ├── LightTheme.kt │ │ └── Theme.kt │ │ ├── util │ │ ├── CustomWindowDecorationAccessing.kt │ │ └── UnsafeAccessing.kt │ │ └── window │ │ ├── JBWindow.Linux.kt │ │ ├── JBWindow.MacOS.kt │ │ ├── JBWindow.Windows.kt │ │ ├── JBWindow.kt │ │ ├── MainToolBar.Basic.kt │ │ ├── MainToolBar.Linux.kt │ │ ├── MainToolBar.MacOS.kt │ │ ├── MainToolBar.Windows.kt │ │ └── MainToolBar.kt │ └── resources │ ├── fonts │ ├── Inter-Black.ttf │ ├── Inter-Bold.ttf │ ├── Inter-ExtraBold.ttf │ ├── Inter-ExtraLight.ttf │ ├── Inter-Light.ttf │ ├── Inter-Medium.ttf │ ├── Inter-Regular.ttf │ ├── Inter-SemiBold.ttf │ └── Inter-Thin.ttf │ └── icons │ ├── buttonDropTriangle.svg │ ├── buttonDropTriangle_dark.svg │ ├── closeSmall.svg │ ├── closeSmallHovered.svg │ ├── closeSmallHovered_dark.svg │ ├── closeSmall_dark.svg │ ├── compose.svg │ ├── external_link_arrow.svg │ ├── external_link_arrow_dark.svg │ ├── linkDropTriangle.svg │ ├── linkDropTriangle_dark.svg │ └── windows │ ├── closeActive.svg │ ├── closeInactive.svg │ ├── collapse.svg │ ├── maximize.svg │ ├── maximizeInactive.svg │ ├── minimize.svg │ ├── minimizeInactive.svg │ ├── restore.svg │ └── restoreInactive.svg ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 4 | push: 5 | branches: [ main ] 6 | # Trigger the workflow on any pull request 7 | pull_request: 8 | 9 | jobs: 10 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 11 | gradleValidation: 12 | name: Gradle Wrapper 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | # Check out current repository 17 | - name: Fetch Sources 18 | uses: actions/checkout@v2.3.4 19 | 20 | # Validate wrapper 21 | - name: Gradle Wrapper Validation 22 | uses: gradle/wrapper-validation-action@v1.0.4 23 | build: 24 | name: Build 25 | runs-on: ubuntu-latest 26 | outputs: 27 | version: ${{ steps.properties.outputs.version }} 28 | changelog: ${{ steps.properties.outputs.changelog }} 29 | steps: 30 | 31 | # Check out current repository 32 | - name: Fetch Sources 33 | uses: actions/checkout@v2 34 | 35 | - name: Download JBR 36 | uses: carlosperate/download-file-action@v2 37 | with: 38 | file-url: 'https://cache-redirector.jetbrains.com/intellij-jbr/jbr-17.0.5-linux-x64-b653.14.tar.gz' 39 | file-name: 'jbr-17.0.5.tar.gz' 40 | 41 | - name: Setup Java 42 | uses: actions/setup-java@v3 43 | with: 44 | java-version: 17 45 | distribution: jdkfile 46 | jdkFile: jbr-17.0.5.tar.gz 47 | cache: gradle 48 | 49 | # Set environment variables 50 | - name: Export Properties 51 | id: properties 52 | shell: bash 53 | run: | 54 | PROPERTIES="$(./gradlew properties -q)" 55 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 56 | NAME="$(echo "$PROPERTIES" | grep "^name:" | cut -f2- -d ' ')" 57 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 58 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 59 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 60 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 61 | echo "version=$VERSION" 62 | echo "name=$NAME" 63 | echo "changelog=$CHANGELOG" 64 | echo "version=$VERSION" >> $GITHUB_OUTPUT 65 | echo "name=$NAME" >> $GITHUB_OUTPUT 66 | echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT 67 | # Build artifact using buildPlugin Gradle task 68 | - name: Build 69 | run: ./gradlew build 70 | 71 | # Store built plugin as an artifact for downloading 72 | - name: Upload artifacts 73 | uses: actions/upload-artifact@v3 74 | with: 75 | name: "${{ steps.properties.outputs.name }} - ${{ steps.properties.outputs.version }}" 76 | path: ./**/build/libs/* 77 | 78 | # Prepare a draft release for GitHub Releases page for the manual verification 79 | # If accepted and published, release workflow would be triggered 80 | releaseDraft: 81 | name: Release Draft 82 | if: github.event_name != 'pull_request' 83 | needs: build 84 | runs-on: ubuntu-latest 85 | steps: 86 | 87 | # Check out current repository 88 | - name: Fetch Sources 89 | uses: actions/checkout@v2.3.4 90 | 91 | # Remove old release drafts by using the curl request for the available releases with draft flag 92 | - name: Remove Old Release Drafts 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | run: | 96 | gh api repos/{owner}/{repo}/releases \ 97 | --jq '.[] | select(.draft == true) | .id' \ 98 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 99 | # Create new release draft - which is not publicly visible and requires manual acceptance 100 | - name: Create Release Draft 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | run: | 104 | gh release create v${{ needs.build.outputs.version }} \ 105 | --draft \ 106 | --title "v${{ needs.build.outputs.version }}" \ 107 | --notes "${{ needs.build.outputs.changelog }}" 108 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared 2 | # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. 3 | 4 | name: Release 5 | on: 6 | release: 7 | types: [ prereleased, released ] 8 | 9 | jobs: 10 | 11 | # Prepare and publish the plugin to the Marketplace repository 12 | release: 13 | name: Publish Plugin 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | # Check out current repository 18 | - name: Fetch Sources 19 | uses: actions/checkout@v2.3.4 20 | with: 21 | ref: ${{ github.event.release.tag_name }} 22 | 23 | - name: Download JBR 24 | uses: carlosperate/download-file-action@v2 25 | with: 26 | file-url: 'https://cache-redirector.jetbrains.com/intellij-jbr/jbr-17.0.5-linux-x64-b653.14.tar.gz' 27 | file-name: 'jbr-17.0.5.tar.gz' 28 | 29 | - name: Setup Java 30 | uses: actions/setup-java@v3 31 | with: 32 | java-version: 17 33 | distribution: jdkfile 34 | jdkFile: jbr-17.0.5.tar.gz 35 | cache: gradle 36 | 37 | # Update Unreleased section with the current release note 38 | - name: Patch Changelog 39 | run: | 40 | ./gradlew patchChangelog --release-note="`cat << EOM 41 | ${{ github.event.release.body }} 42 | EOM`" 43 | 44 | - name: Import GPG key 45 | uses: crazy-max/ghaction-import-gpg@v3 46 | with: 47 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 48 | 49 | - uses: ButterCam/setup-sisyphus-build@v1 50 | with: 51 | dependency-repositories: local,central,portal,google,snapshot 52 | snapshot-url: https://s01.oss.sonatype.org/content/repositories/snapshots 53 | snapshot-username: ${{ secrets.OSSRH_USERNAME }} 54 | snapshot-password: ${{ secrets.OSSRH_PASSWORD }} 55 | release-url: https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 56 | release-username: ${{ secrets.OSSRH_USERNAME }} 57 | release-password: ${{ secrets.OSSRH_PASSWORD }} 58 | gradle-portal-key: ${{ secrets.GRADLE_PUBLISH_KEY }} 59 | gradle-portal-secret: ${{ secrets.GRADLE_PUBLISH_SECRET }} 60 | gpg-key-name: ${{ secrets.GPG_KEY_NAME }} 61 | 62 | # Publish the plugin to the Marketplace 63 | - name: Publish 64 | env: 65 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 66 | run: ./gradlew publish 67 | 68 | # Upload artifact as a release asset 69 | - name: Upload Release Asset 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | run: gh release upload ${{ github.event.release.tag_name }} ./**/build/libs/* 73 | 74 | # Create pull request 75 | - name: Create Pull Request 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | run: | 79 | VERSION="${{ github.event.release.tag_name }}" 80 | BRANCH="changelog-update-$VERSION" 81 | git config user.email "action@github.com" 82 | git config user.name "GitHub Action" 83 | git checkout -b $BRANCH 84 | git commit -am "Changelog update - $VERSION" 85 | git push --set-upstream origin $BRANCH 86 | gh pr create \ 87 | --title ":bookmark: Changelog update - \`$VERSION\`" \ 88 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 89 | --base main \ 90 | --head $BRANCH -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !foundation/gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ 26 | 27 | logs/ 28 | out/ 29 | data/elasticsearch 30 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [2.2.0] 6 | - Upgrade to compose 1.5.2 7 | 8 | ## [2.1.0] 9 | - Provide BasicMainToolBar #18 10 | - Disable minimize and maximize button when resizeable is false #19 11 | - Fix the custom title bar when MainToolBar Content is empty #20 12 | - Optimize the implementation of themes. Switching themes will not cause full recompose. 13 | - Add text styles for theme 14 | 15 | ## [2.0.0] 16 | - Exp UI theme support 17 | 18 | ## [1.1.0] 19 | - Target to jvm 1.11 #11 20 | - Replacing SVG Resources with Compose generated Image Vectors #13 21 | - IntelliJ Dracula colors (Dark Theme) #12 22 | 23 | Thanks @DevSrSouza 24 | 25 | ## [1.0.1] 26 | - Upgrade to compose 1.0.1 27 | 28 | ## [1.0.0] 29 | - JetBrains UI Kit for Compose desktop -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ButterCam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # We are planning to migrate to [JetBrains/jewel](https://github.com/JetBrains/jewel)! 2 | 3 | We are discussing with JetBrains officials about migrating this project to [JetBrains/jewel](https://github.com/JetBrains/jewel). This will be the focus of our next work. 4 | 5 | Track progress in this issue: JetBrains/jewel#28 6 | 7 | # JetBrains UI Kit for Compose Desktop 8 | 9 | New JetBrains style controls and UI kits for [Compose Desktop](https://www.jetbrains.com/lp/compose/). 10 | 11 | Classic JetBrains UI kit have been moved to [here](classic). 12 | 13 | ![screenshot](docs/screenshot-expui.png) 14 | 15 | ## Quick Start 16 | 17 | ## Requirements 18 | 19 | ### JetBrains Runtime 20 | 21 | To get the best presentation across platforms, this library requires the application to run 22 | on [JetBrains Runtime](https://github.com/JetBrains/JetBrainsRuntime) (JBR). 23 | 24 | [JetBrains Runtime](https://github.com/JetBrains/JetBrainsRuntime) is a modified version of the JetBrains JDK 25 | distribution, which is widely used across the JetBrains' IDEs such as IntelliJ, GoLand and others. 26 | 27 | In this library we use the additional API provided by JetBrains Runtime to customise the title bar in Windows and macOS. 28 | 29 | If the `JBWindow` component is not used, it can also be used without the JetBrains Runtime. 30 | 31 | When using `JBWindow` in a non-JetBrains Runtime environment, a native title bar may additionally be displayed. 32 | 33 | JBR-17 (corresponding to JDK 17) can be downloaded from [the SDK page](https://www.jetbrains.com/help/idea/sdk.html) of 34 | IntelliJ. 35 | 36 | Make sure that the [project SDK](https://www.jetbrains.com/help/idea/project-settings-and-structure.html#project-sdk) 37 | and [Gradle JVM](https://www.jetbrains.com/help/idea/gradle-jvm-selection.html) are both JBR-17. 38 | 39 | #### Project SDK settings 40 | 41 | ![](docs/project-sdk.png) 42 | 43 | #### Gradle JVM settings 44 | 45 | ![](docs/gradle-jvm.png) 46 | 47 | ### `jdk.unsupported` module 48 | 49 | Most of the JetBrains Runtime APIs are private to JetBrains, and the class `sun.misc.Unsafe` is used to get access to 50 | these APIs. 51 | 52 | You need to add the `jdk.unsupported` module to the compose dependency as in the following code to make it work. 53 | 54 | You can also skip this step if you don't need to use the `JBWindow` component. 55 | 56 | ```kotlin 57 | compose.desktop { 58 | application { 59 | nativeDistributions { 60 | modules("jdk.unsupported") 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ## 1. Add dependency 67 | 68 | ```kotlin 69 | dependencies { 70 | implementation(compose.desktop.currentOs) { 71 | exclude("org.jetbrains.compose.material") 72 | } 73 | implementation("com.bybutter.compose:compose-jetbrains-expui-theme:2.0.0") 74 | } 75 | ``` 76 | 77 | ## 2. Use JBWindow DSL 78 | 79 | The optimal display of JBWindow requires JetBrains Runtime, refer to the previous [requirements](#Requirements). 80 | 81 | ```kotlin 82 | fun main() = application { 83 | JBWindow( 84 | title = "JetBrains ExpUI Gallery", 85 | showTitle = true, // If you want to render your own component in the center of the title bar like Intellij do, disable this to hide the title of the MainToolBar (TitleBar). 86 | theme = LightTheme, // Change the theme here, LightTheme and DarkTheme are provided. 87 | state = rememberWindowState(size = DpSize(900.dp, 700.dp)), 88 | onCloseRequest = { 89 | exitApplication() 90 | }, 91 | mainToolBar = { 92 | // Render your own component in the MainToolBar (TitleBar). 93 | Row( 94 | Modifier.mainToolBarItem( 95 | Alignment.End, 96 | true 97 | ) 98 | ) { // Use the mainToolBarItem modifier to change alignment of components and enable/disable window drag area on this component. 99 | } 100 | }) { 101 | // Window content 102 | } 103 | } 104 | ``` 105 | 106 | ## 3. Use LightTheme and DarkTheme DSL 107 | 108 | Since `JBWindow` requires JetBrains Runtime, if you don't want to use the `JBWindow` component, you can also use a 109 | normal `Window` with `LightTheme`/`DarkTheme` DSL. 110 | 111 | ```kotlin 112 | fun main() = application { 113 | Window({}) { 114 | LightTheme { 115 | // Your components here 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | 122 | # Screenshot 123 | 124 | ## MacOS 125 | 126 | 截屏2022-11-21 17 11 56 127 | 截屏2022-11-21 17 12 18 128 | 129 | ## Windows 130 | 131 | ![image](https://user-images.githubusercontent.com/9367842/203374673-be0e25ab-de0c-4682-a511-bbadf0949446.png) 132 | ![image](https://user-images.githubusercontent.com/9367842/203374902-0b384bdd-fff4-4cbc-a8eb-93d6b58229a2.png) 133 | 134 | ## Linux(Ubuntu with Gnome) 135 | 136 | ![image](https://user-images.githubusercontent.com/9367842/203574847-6b6a3d4a-0da2-41f8-ac3f-9a8402f26b16.png) 137 | ![image](https://user-images.githubusercontent.com/9367842/203574918-953092bc-b00f-455b-a43a-237c63093095.png) 138 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.bybutter.sisyphus.project") version "2.1.0" apply false 3 | id("com.netflix.nebula.contacts") version "7.0.1" 4 | id("com.netflix.nebula.info") version "12.1.6" apply false 5 | id("com.netflix.nebula.maven-publish") version "20.3.0" apply false 6 | id("com.netflix.nebula.source-jar") version "20.3.0" apply false 7 | id("org.jetbrains.changelog") version "1.3.0" 8 | id("org.jetbrains.compose") version "1.5.2" apply false 9 | kotlin("jvm") version "1.9.10" apply false 10 | } 11 | 12 | allprojects { 13 | apply(plugin = "com.netflix.nebula.contacts") 14 | apply(plugin = "com.netflix.nebula.info") 15 | 16 | group = "com.bybutter.compose" 17 | version = "2.2.0" 18 | 19 | contacts { 20 | addPerson( 21 | "higan@live.cn", 22 | delegateClosureOf { 23 | moniker = "higan" 24 | github = "devkanro" 25 | roles.add("owner") 26 | }, 27 | ) 28 | } 29 | } 30 | 31 | subprojects { 32 | apply(plugin = "org.jetbrains.compose") 33 | 34 | tasks.withType() { 35 | kotlinOptions.jvmTarget = "17" 36 | } 37 | 38 | repositories { 39 | mavenLocal() 40 | mavenCentral() 41 | google() 42 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 43 | } 44 | } 45 | 46 | changelog { 47 | version.set(project.version.toString()) 48 | groups.set(emptyList()) 49 | } 50 | -------------------------------------------------------------------------------- /classic/README.md: -------------------------------------------------------------------------------- 1 | # JetBrains UI Kit for Compose Desktop 2 | 3 | JetBrains style controls and UI for [Compose Desktop](https://www.jetbrains.com/lp/compose/). 4 | 5 | ![screenshot](../docs/screenshot.png) 6 | 7 | ## Quick Start 8 | 9 | ## 1. Add dependency 10 | 11 | ```kotlin 12 | dependencies { 13 | implementation(compose.desktop.currentOs) { 14 | exclude("org.jetbrains.compose.material") 15 | } 16 | implementation("com.bybutter.compose:compose-jetbrains-theme:2.0.0") 17 | } 18 | ``` 19 | 20 | ## 2. JBTheme DSL 21 | 22 | ```kotlin 23 | fun main() = application { 24 | Window( 25 | onCloseRequest = ::exitApplication, 26 | title = "Compose for Desktop", 27 | state = rememberWindowState(width = 300.dp, height = 300.dp) 28 | ) { 29 | val count = remember { mutableStateOf(0) } 30 | 31 | JBTheme { 32 | JPanel(Modifier.fillMaxSize().jBorder(top = 1.dp, color = JBTheme.panelColors.border)) { 33 | Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { 34 | Button(modifier = Modifier.align(Alignment.CenterHorizontally), 35 | onClick = { 36 | count.value++ 37 | }) { 38 | Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!") 39 | } 40 | Button(modifier = Modifier.align(Alignment.CenterHorizontally), 41 | onClick = { 42 | count.value = 0 43 | }) { 44 | Text("Reset") 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | ``` -------------------------------------------------------------------------------- /classic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | `java-library` 4 | id("com.netflix.nebula.maven-publish") 5 | id("com.netflix.nebula.source-jar") 6 | id("com.bybutter.sisyphus.project") 7 | id("org.jetbrains.compose") 8 | } 9 | 10 | description = "JetBrains UI Kit for Compose Desktop" 11 | 12 | dependencies { 13 | implementation(kotlin("stdlib")) 14 | implementation(compose.desktop.common) { 15 | exclude("org.jetbrains.compose.material") 16 | } 17 | } 18 | 19 | tasks.withType() { 20 | kotlinOptions.jvmTarget = "17" 21 | kotlinOptions.freeCompilerArgs += listOf( 22 | // "-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true" 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/JBTypography.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.text.style.TextDecoration 8 | import androidx.compose.ui.unit.sp 9 | 10 | class JBTypography( 11 | val h0: TextStyle, 12 | val h1: TextStyle, 13 | val h2: TextStyle, 14 | val h2Bold: TextStyle, 15 | val h3: TextStyle, 16 | val h3Bold: TextStyle, 17 | val default: TextStyle, 18 | val defaultBold: TextStyle, 19 | val defaultUnderlined: TextStyle, 20 | val paragraph: TextStyle, 21 | val medium: TextStyle, 22 | val mediumBold: TextStyle, 23 | val small: TextStyle, 24 | val smallUnderlined: TextStyle, 25 | ) { 26 | constructor( 27 | defaultFontFamily: FontFamily = FontFamily.Default, 28 | h0: TextStyle = TextStyle( 29 | fontWeight = FontWeight.Medium, 30 | fontSize = 25.sp 31 | ), 32 | h1: TextStyle = TextStyle( 33 | fontWeight = FontWeight.Medium, 34 | fontSize = 22.sp 35 | ), 36 | h2: TextStyle = TextStyle( 37 | fontWeight = FontWeight.Normal, 38 | fontSize = 18.sp, 39 | ), 40 | h2Bold: TextStyle = h2.copy(fontWeight = FontWeight.Medium), 41 | h3: TextStyle = TextStyle( 42 | fontWeight = FontWeight.Normal, 43 | fontSize = 16.sp, 44 | lineHeight = 20.sp 45 | ), 46 | h3Bold: TextStyle = h3.copy(fontWeight = FontWeight.Medium), 47 | default: TextStyle = TextStyle( 48 | fontWeight = FontWeight.Normal, 49 | fontSize = 12.5.sp, 50 | lineHeight = 15.sp 51 | ), 52 | defaultBold: TextStyle = default.copy(fontWeight = FontWeight.Medium), 53 | defaultUnderlined: TextStyle = default.copy(textDecoration = TextDecoration.Underline), 54 | paragraph: TextStyle = TextStyle( 55 | fontWeight = FontWeight.Normal, 56 | fontSize = 13.sp, 57 | lineHeight = 19.sp 58 | ), 59 | medium: TextStyle = TextStyle( 60 | fontWeight = FontWeight.Normal, 61 | fontSize = 12.sp, 62 | lineHeight = 15.sp 63 | ), 64 | mediumBold: TextStyle = medium.copy(fontWeight = FontWeight.Medium), 65 | small: TextStyle = TextStyle( 66 | fontWeight = FontWeight.Normal, 67 | fontSize = 11.sp, 68 | lineHeight = 14.sp 69 | ), 70 | smallUnderlined: TextStyle = small.copy(textDecoration = TextDecoration.Underline), 71 | ) : this( 72 | h0.withDefaultFontFamily(defaultFontFamily), 73 | h1.withDefaultFontFamily(defaultFontFamily), 74 | h2.withDefaultFontFamily(defaultFontFamily), 75 | h2Bold.withDefaultFontFamily(defaultFontFamily), 76 | h3.withDefaultFontFamily(defaultFontFamily), 77 | h3Bold.withDefaultFontFamily(defaultFontFamily), 78 | default.withDefaultFontFamily(defaultFontFamily), 79 | defaultBold.withDefaultFontFamily(defaultFontFamily), 80 | defaultUnderlined.withDefaultFontFamily(defaultFontFamily), 81 | paragraph.withDefaultFontFamily(defaultFontFamily), 82 | medium.withDefaultFontFamily(defaultFontFamily), 83 | mediumBold.withDefaultFontFamily(defaultFontFamily), 84 | small.withDefaultFontFamily(defaultFontFamily), 85 | smallUnderlined.withDefaultFontFamily(defaultFontFamily) 86 | ) 87 | 88 | companion object { 89 | private fun TextStyle.withDefaultFontFamily(default: FontFamily): TextStyle { 90 | return if (fontFamily != null) this else copy(fontFamily = default) 91 | } 92 | } 93 | } 94 | 95 | val LocalTypography = compositionLocalOf { JBTypography() } 96 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/CheckBoxColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class CheckBoxColors( 10 | bg: Color, 11 | bgSelected: Color, 12 | bgDisabled: Color, 13 | border: Color, 14 | borderSelected: Color, 15 | borderFocused: Color, 16 | borderDisabled: Color, 17 | ) { 18 | var bg by mutableStateOf(bg) 19 | var bgSelected by mutableStateOf(bgSelected) 20 | var bgDisabled by mutableStateOf(bgDisabled) 21 | var border by mutableStateOf(border) 22 | var borderSelected by mutableStateOf(borderSelected) 23 | var borderFocused by mutableStateOf(borderFocused) 24 | var borderDisabled by mutableStateOf(borderDisabled) 25 | 26 | fun copy( 27 | bg: Color = this.bg, 28 | bgSelected: Color = this.bgSelected, 29 | bgDisabled: Color = this.bgDisabled, 30 | border: Color = this.border, 31 | borderSelected: Color = this.borderSelected, 32 | borderFocused: Color = this.borderFocused, 33 | borderDisabled: Color = this.borderDisabled, 34 | ): CheckBoxColors { 35 | return CheckBoxColors( 36 | bg, 37 | bgSelected, 38 | bgDisabled, 39 | border, 40 | borderSelected, 41 | borderFocused, 42 | borderDisabled 43 | ) 44 | } 45 | } 46 | 47 | fun lightCheckBoxColors(): CheckBoxColors { 48 | return CheckBoxColors( 49 | Color.White, 50 | Color(0xFF4F9EE3), 51 | Color(0xFFF2F2F2), 52 | Color(0xFFB0B0B0), 53 | Color(0xFF4B97D9), 54 | Color(0xFF7B9FC7), 55 | Color(0xFFBDBDBD), 56 | ) 57 | } 58 | 59 | fun darkCheckBoxColors(): CheckBoxColors { 60 | return CheckBoxColors( 61 | Color(0xFF43494A), 62 | Color(0xFF43494A), 63 | Color(0xFF3C3F41), 64 | Color(0xFF6B6B6B), 65 | Color(0xFF4C708C), 66 | Color(0xFF43698E), 67 | Color(0xFF545556), 68 | ) 69 | } 70 | 71 | val LocalCheckBoxColors = compositionLocalOf { lightCheckBoxColors() } 72 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/FieldColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.interaction.InteractionSource 6 | import androidx.compose.foundation.interaction.collectIsFocusedAsState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.State 9 | import androidx.compose.runtime.compositionLocalOf 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.rememberUpdatedState 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.graphics.Color 15 | import io.kanro.compose.jetbrains.JBTheme 16 | 17 | class FieldColors( 18 | bg: Color, 19 | border: Color, 20 | borderFocused: Color, 21 | comboboxButton: Color, 22 | bgDisabled: Color, 23 | borderDisabled: Color, 24 | borderError: Color, 25 | ) { 26 | var bg by mutableStateOf(bg) 27 | var border by mutableStateOf(border) 28 | var borderFocused by mutableStateOf(borderFocused) 29 | var comboboxButton by mutableStateOf(comboboxButton) 30 | var bgDisabled by mutableStateOf(bgDisabled) 31 | var borderDisabled by mutableStateOf(borderDisabled) 32 | var borderError by mutableStateOf(borderError) 33 | 34 | fun copy( 35 | bg: Color = this.bg, 36 | border: Color = this.border, 37 | borderFocused: Color = this.borderFocused, 38 | comboboxButton: Color = this.comboboxButton, 39 | bgDisabled: Color = this.bgDisabled, 40 | borderDisabled: Color = this.borderDisabled, 41 | borderError: Color = this.borderError, 42 | ): FieldColors { 43 | return FieldColors(bg, border, borderFocused, comboboxButton, bgDisabled, borderDisabled, borderError) 44 | } 45 | 46 | @Composable 47 | fun bgColor(enable: Boolean): State { 48 | return rememberUpdatedState(if (enable) bg else bgDisabled) 49 | } 50 | 51 | @Composable 52 | fun borderColor(enable: Boolean, error: Boolean, interactionSource: InteractionSource): State { 53 | val focused by interactionSource.collectIsFocusedAsState() 54 | val targetValue = when { 55 | !enable -> borderDisabled 56 | error -> borderError 57 | focused -> borderFocused 58 | else -> border 59 | } 60 | return if (enable) { 61 | animateColorAsState(targetValue, tween(durationMillis = AnimationDuration)) 62 | } else { 63 | rememberUpdatedState(targetValue) 64 | } 65 | } 66 | 67 | @Composable 68 | fun focusingColor(enable: Boolean, error: Boolean, interactionSource: InteractionSource): State { 69 | val focused by interactionSource.collectIsFocusedAsState() 70 | val targetValue = when { 71 | !enable -> Color.Transparent 72 | error -> JBTheme.focusColors.error 73 | focused -> JBTheme.focusColors.default 74 | else -> Color.Transparent 75 | } 76 | return rememberUpdatedState(targetValue) 77 | } 78 | 79 | companion object { 80 | internal const val AnimationDuration = 150 81 | } 82 | } 83 | 84 | fun lightFieldColors(): FieldColors { 85 | return FieldColors( 86 | Color.White, 87 | Color(0xFFC4C4C4), 88 | Color(0xFF87AFDA), 89 | Color(0xFFFAFAFA), 90 | Color(0xFFF2F2F2), 91 | Color(0xFFCFCFCF), 92 | Color(0xFFCE3845), 93 | ) 94 | } 95 | 96 | fun darkFieldColors(): FieldColors { 97 | return FieldColors( 98 | Color(0xFF4C5052), 99 | Color(0xFF5E6060), 100 | Color(0xFF456A90), 101 | Color(0xFF3C3F41), 102 | Color(0xFF3C3F41), 103 | Color(0xFF646464), 104 | Color(0xFF73454B), 105 | ) 106 | } 107 | 108 | val LocalFieldColors = compositionLocalOf { lightFieldColors() } 109 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/FocusColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class FocusColors( 10 | default: Color, 11 | error: Color, 12 | warning: Color, 13 | warningInactive: Color, 14 | ) { 15 | var default by mutableStateOf(default) 16 | var error by mutableStateOf(error) 17 | var warning by mutableStateOf(warning) 18 | var warningInactive by mutableStateOf(warningInactive) 19 | 20 | fun copy( 21 | default: Color = this.default, 22 | error: Color = this.error, 23 | warning: Color = this.warning, 24 | warningInactive: Color = this.warningInactive, 25 | ): FocusColors { 26 | return FocusColors(default, error, warning, warningInactive) 27 | } 28 | } 29 | 30 | fun lightFocusColors(): FocusColors { 31 | return FocusColors( 32 | Color(0xFF97C3F3), 33 | Color(0xFFE53E4D), 34 | Color(0xFFE1A336), 35 | Color(0xFFEAD2A1), 36 | ) 37 | } 38 | 39 | fun darkFocusColors(): FocusColors { 40 | return FocusColors( 41 | Color(0xFF3D6185), 42 | Color(0xFF8B3C3C), 43 | Color(0xFFAC7920), 44 | Color(0xFF6E5324), 45 | ) 46 | } 47 | 48 | val LocalFocusColors = compositionLocalOf { lightFocusColors() } 49 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/IconColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class IconColors( 10 | selected: Color, 11 | disabled: Color, 12 | ) { 13 | var selected by mutableStateOf(selected) 14 | var disabled by mutableStateOf(disabled) 15 | 16 | fun copy( 17 | selected: Color = this.selected, 18 | disabled: Color = this.disabled, 19 | ): IconColors { 20 | return IconColors( 21 | selected, 22 | disabled, 23 | ) 24 | } 25 | } 26 | 27 | fun lightIconColors(): IconColors { 28 | return IconColors( 29 | Color.White, 30 | Color(0xFFABABAB), 31 | ) 32 | } 33 | 34 | fun darkIconColors(): IconColors { 35 | return IconColors( 36 | Color(0xFFFEFEFE), 37 | Color(0xFFABABAB), 38 | ) 39 | } 40 | 41 | val LocalIconColors = compositionLocalOf { lightIconColors() } 42 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/PanelColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class PanelColors( 10 | border: Color, 11 | bgContent: Color, 12 | bgDialog: Color, 13 | ) { 14 | var border by mutableStateOf(border) 15 | var bgContent by mutableStateOf(bgContent) 16 | var bgDialog by mutableStateOf(bgDialog) 17 | 18 | fun copy( 19 | border: Color = this.border, 20 | bgContent: Color = this.bgContent, 21 | bgDialog: Color = this.bgDialog, 22 | ): PanelColors { 23 | return PanelColors(border, bgContent, bgDialog) 24 | } 25 | } 26 | 27 | fun lightPanelColors(): PanelColors { 28 | return PanelColors( 29 | Color(0xFFD1D1D1), 30 | Color.White, 31 | Color(0xFFF2F2F2), 32 | ) 33 | } 34 | 35 | fun darkPanelColors(): PanelColors { 36 | return PanelColors( 37 | Color(0xFF323232), 38 | Color(0xFF3C3F41), 39 | Color(0xFF3C3F41), 40 | ) 41 | } 42 | 43 | val LocalPanelColors = compositionLocalOf { lightPanelColors() } 44 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ProgressColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class ProgressColors( 10 | progress: Color, 11 | bg: Color, 12 | ) { 13 | var progress by mutableStateOf(progress) 14 | var bg by mutableStateOf(bg) 15 | 16 | fun copy( 17 | progress: Color = this.progress, 18 | bg: Color = this.bg, 19 | ): ProgressColors { 20 | return ProgressColors(progress, bg) 21 | } 22 | } 23 | 24 | fun lightProgressColors(): ProgressColors { 25 | return ProgressColors( 26 | Color(0xFF1E82E6), 27 | Color(0xFFD5D5D5), 28 | ) 29 | } 30 | 31 | fun darkProgressColors(): ProgressColors { 32 | return ProgressColors( 33 | Color(0xFFA0A0A0), 34 | Color(0xFF555555), 35 | ) 36 | } 37 | 38 | val LocalProgressColors = compositionLocalOf { lightProgressColors() } 39 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ScrollColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.foundation.ScrollbarStyle 4 | import androidx.compose.foundation.shape.CircleShape 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.setValue 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.unit.dp 11 | 12 | class ScrollColors( 13 | bg: Color, 14 | ) { 15 | var bg by mutableStateOf(bg) 16 | 17 | fun copy( 18 | bg: Color = this.bg, 19 | ): ScrollColors { 20 | return ScrollColors(bg) 21 | } 22 | 23 | fun style(): ScrollbarStyle { 24 | return ScrollbarStyle( 25 | minimalHeight = 30.dp, 26 | thickness = 7.dp, 27 | shape = CircleShape, 28 | hoverDurationMillis = 0, 29 | unhoverColor = bg, 30 | hoverColor = bg 31 | ) 32 | } 33 | } 34 | 35 | fun lightScrollColors(): ScrollColors { 36 | return ScrollColors( 37 | Color(0xFFC9C9C9), 38 | ) 39 | } 40 | 41 | fun darkScrollColors(): ScrollColors { 42 | return ScrollColors( 43 | Color(0xFF494949) 44 | ) 45 | } 46 | 47 | val LocalScrollColors = compositionLocalOf { lightScrollColors() } 48 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/SelectionColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class SelectionColors( 10 | active: Color, 11 | inactive: Color, 12 | hover: Color, 13 | lightActive: Color, 14 | lightInactive: Color, 15 | completionPopup: Color, 16 | ) { 17 | var active by mutableStateOf(active) 18 | var inactive by mutableStateOf(inactive) 19 | var hover by mutableStateOf(hover) 20 | var lightActive by mutableStateOf(lightActive) 21 | var lightInactive by mutableStateOf(lightInactive) 22 | var completionPopup by mutableStateOf(completionPopup) 23 | 24 | fun copy( 25 | active: Color = this.active, 26 | inactive: Color = this.inactive, 27 | hover: Color = this.hover, 28 | lightActive: Color = this.lightActive, 29 | lightInactive: Color = this.lightInactive, 30 | completionPopup: Color = this.completionPopup, 31 | ): SelectionColors { 32 | return SelectionColors(active, inactive, hover, lightActive, lightInactive, completionPopup) 33 | } 34 | } 35 | 36 | fun lightSelectionColors(): SelectionColors { 37 | return SelectionColors( 38 | Color(0xFF2675BF), 39 | Color(0xFFD5D5D5), 40 | Color(0xFFEDF5FC), 41 | Color(0xFFEDF6FE), 42 | Color(0xFFF5F5F5), 43 | Color(0xFFC5DFFC), 44 | ) 45 | } 46 | 47 | fun darkSelectionColors(): SelectionColors { 48 | return SelectionColors( 49 | Color(0xFF2F65CA), 50 | Color(0xFF0D293E), 51 | Color(0xFF464A4D), 52 | Color(0xFF464A4D), 53 | Color(0xFF35383B), 54 | Color(0xFF113A5C), 55 | ) 56 | } 57 | 58 | val LocalSelectionColors = compositionLocalOf { lightSelectionColors() } 59 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/TabColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class TabColors( 10 | selection: Color, 11 | focus: Color, 12 | selectionInactive: Color, 13 | hover: Color, 14 | selectionDisabled: Color, 15 | bgSelected: Color, 16 | ) { 17 | var selection by mutableStateOf(selection) 18 | var focus by mutableStateOf(focus) 19 | var selectionInactive by mutableStateOf(selectionInactive) 20 | var hover by mutableStateOf(hover) 21 | var selectionDisabled by mutableStateOf(selectionDisabled) 22 | var bgSelected by mutableStateOf(bgSelected) 23 | 24 | fun copy( 25 | selection: Color = this.selection, 26 | focus: Color = this.focus, 27 | selectionInactive: Color = this.selectionInactive, 28 | hover: Color = this.hover, 29 | selectionDisabled: Color = this.selectionDisabled, 30 | bgSelected: Color = this.bgSelected, 31 | ): TabColors { 32 | return TabColors(selection, focus, selectionInactive, hover, selectionDisabled, bgSelected) 33 | } 34 | } 35 | 36 | fun lightTabColors(): TabColors { 37 | return TabColors( 38 | Color(0xFF4083C9), 39 | Color(0xFFDAE4ED), 40 | Color(0xFF9CA7B8), 41 | Color(0x19000000), 42 | Color(0xFFABABAB), 43 | Color(0xFFFFFFFF), 44 | ) 45 | } 46 | 47 | fun darkTabColors(): TabColors { 48 | return TabColors( 49 | Color(0xFF4A88C7), 50 | Color(0xFF3D4B5C), 51 | Color(0xFF747A80), 52 | Color(0xFF2E3133), 53 | Color(0xFF595959), 54 | Color(0xFF4E5254), 55 | ) 56 | } 57 | 58 | val LocalTabColors = compositionLocalOf { lightTabColors() } 59 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/TableColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.compose.ui.graphics.Color 7 | 8 | class TableColors( 9 | bg: Color, 10 | headerBorder: Color, 11 | outerBorder: Color, 12 | ) { 13 | var bg by mutableStateOf(bg) 14 | var headerBorder by mutableStateOf(headerBorder) 15 | var outerBorder by mutableStateOf(outerBorder) 16 | } 17 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/TextColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.graphics.Color 8 | 9 | class TextColors( 10 | default: Color, 11 | disabled: Color, 12 | white: Color, 13 | link: Color, 14 | infoPanel: Color, 15 | infoInput: Color, 16 | error: Color, 17 | success: Color, 18 | ) { 19 | var default by mutableStateOf(default) 20 | var disabled by mutableStateOf(disabled) 21 | var white by mutableStateOf(white) 22 | var link by mutableStateOf(link) 23 | var infoPanel by mutableStateOf(infoPanel) 24 | var infoInput by mutableStateOf(infoInput) 25 | var error by mutableStateOf(error) 26 | var success by mutableStateOf(success) 27 | 28 | fun copy( 29 | default: Color = this.default, 30 | disabled: Color = this.disabled, 31 | white: Color = this.white, 32 | link: Color = this.link, 33 | infoPanel: Color = this.infoPanel, 34 | infoInput: Color = this.infoInput, 35 | error: Color = this.error, 36 | success: Color = this.success, 37 | ): TextColors { 38 | return TextColors(default, disabled, white, link, infoPanel, infoInput, error, success) 39 | } 40 | } 41 | 42 | fun lightTextColors(): TextColors { 43 | return TextColors( 44 | Color.Black, 45 | Color(0xFF8C8C8C), 46 | Color.White, 47 | Color(0xFF2470B3), 48 | Color(0xFF808080), 49 | Color(0xFF999999), 50 | Color(0xFFC7222D), 51 | Color(0xFF368746), 52 | ) 53 | } 54 | 55 | fun darkTextColors(): TextColors { 56 | return TextColors( 57 | Color(0xFFBBBBBB), 58 | Color(0xFF777777), 59 | Color(0xFFFEFEFE), 60 | Color(0xFF589DF6), 61 | Color(0xFF8C8C8C), 62 | Color(0xFF787878), 63 | Color(0xFFFF5261), 64 | Color(0xFF50A661), 65 | ) 66 | } 67 | 68 | val LocalTextColors = compositionLocalOf { lightTextColors() } 69 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ToggleColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.compose.ui.graphics.Color 7 | 8 | class ToggleColors( 9 | bg: Color, 10 | off: Color, 11 | on: Color, 12 | ) { 13 | var bg by mutableStateOf(bg) 14 | var off by mutableStateOf(off) 15 | var on by mutableStateOf(on) 16 | } 17 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ToolBarColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.color 2 | 3 | import androidx.compose.foundation.interaction.InteractionSource 4 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 5 | import androidx.compose.foundation.interaction.collectIsPressedAsState 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.State 8 | import androidx.compose.runtime.compositionLocalOf 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.rememberUpdatedState 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.graphics.Color 14 | 15 | class ToolBarColors( 16 | buttonPressed: Color, 17 | buttonHover: Color, 18 | iconSplitBorder: Color, 19 | ) { 20 | var buttonPressed by mutableStateOf(buttonPressed) 21 | var buttonHover by mutableStateOf(buttonHover) 22 | var iconSplitBorder by mutableStateOf(iconSplitBorder) 23 | 24 | fun copy( 25 | buttonPressed: Color = this.buttonPressed, 26 | buttonHover: Color = this.buttonHover, 27 | iconSplitBorder: Color = this.iconSplitBorder, 28 | ): ToolBarColors { 29 | return ToolBarColors(buttonPressed, buttonHover, iconSplitBorder) 30 | } 31 | 32 | @Composable 33 | fun actionIconBgColor(interactionSource: InteractionSource): State { 34 | val pressed by interactionSource.collectIsPressedAsState() 35 | val hover by interactionSource.collectIsHoveredAsState() 36 | val targetValue = when { 37 | pressed -> buttonPressed 38 | hover -> buttonHover 39 | else -> Color.Transparent 40 | } 41 | return rememberUpdatedState(targetValue) 42 | } 43 | } 44 | 45 | fun lightToolBarColors(): ToolBarColors { 46 | return ToolBarColors( 47 | Color(0xFFCFCFCF), 48 | Color(0xFFDFDFDF), 49 | Color(0xFFB3B3B3), 50 | ) 51 | } 52 | 53 | fun darkToolBarColors(): ToolBarColors { 54 | return ToolBarColors( 55 | Color(0xFF5C6164), 56 | Color(0xFF4C5052), 57 | Color(0xFF6B6B6B), 58 | ) 59 | } 60 | 61 | val LocalToolBarColors = compositionLocalOf { lightToolBarColors() } 62 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ActionButton.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.Indication 5 | import androidx.compose.foundation.IndicationInstance 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.hoverable 10 | import androidx.compose.foundation.interaction.InteractionSource 11 | import androidx.compose.foundation.interaction.MutableInteractionSource 12 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 13 | import androidx.compose.foundation.interaction.collectIsPressedAsState 14 | import androidx.compose.foundation.layout.Arrangement 15 | import androidx.compose.foundation.layout.Box 16 | import androidx.compose.foundation.layout.PaddingValues 17 | import androidx.compose.foundation.layout.Row 18 | import androidx.compose.foundation.layout.RowScope 19 | import androidx.compose.foundation.layout.defaultMinSize 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.foundation.shape.RoundedCornerShape 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.CompositionLocalProvider 24 | import androidx.compose.runtime.State 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.graphics.Shape 31 | import androidx.compose.ui.graphics.drawOutline 32 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 33 | import androidx.compose.ui.unit.dp 34 | import io.kanro.compose.jetbrains.JBTheme 35 | 36 | object ActionButtonIndication : Indication { 37 | private class ActionButtonIndicationInstance( 38 | private val shape: Shape, 39 | private val isHover: State, 40 | private val isPressed: State, 41 | private val hoverColor: Color, 42 | private val pressedColor: Color, 43 | ) : IndicationInstance { 44 | override fun ContentDrawScope.drawIndication() { 45 | when { 46 | isPressed.value -> { 47 | val outline = shape.createOutline(size, layoutDirection, this) 48 | drawOutline(outline, pressedColor) 49 | } 50 | 51 | isHover.value -> { 52 | val outline = shape.createOutline(size, layoutDirection, this) 53 | drawOutline(outline, hoverColor) 54 | } 55 | } 56 | drawContent() 57 | } 58 | } 59 | 60 | @Composable 61 | override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { 62 | val shape = remember { RoundedCornerShape(3.dp) } 63 | val isPressed = interactionSource.collectIsPressedAsState() 64 | val isHover = interactionSource.collectIsHoveredAsState() 65 | val hoverColor = JBTheme.toolBarColors.buttonHover 66 | val pressedColor = JBTheme.toolBarColors.buttonPressed 67 | return remember(JBTheme.toolBarColors, interactionSource) { 68 | ActionButtonIndicationInstance( 69 | shape, 70 | isHover, 71 | isPressed, 72 | hoverColor, 73 | pressedColor 74 | ) 75 | } 76 | } 77 | } 78 | 79 | object ActionButtonDefaults { 80 | private val ButtonHorizontalPadding = 3.dp 81 | private val ButtonVerticalPadding = 3.dp 82 | 83 | val MinWidth = 22.dp 84 | 85 | val MinHeight = 22.dp 86 | 87 | val IconSize = 16.dp 88 | 89 | val ContentPadding = PaddingValues( 90 | start = ButtonHorizontalPadding, 91 | top = ButtonVerticalPadding, 92 | end = ButtonHorizontalPadding, 93 | bottom = ButtonVerticalPadding 94 | ) 95 | } 96 | 97 | @Composable 98 | fun ActionButton( 99 | onClick: () -> Unit, 100 | modifier: Modifier = Modifier, 101 | enabled: Boolean = true, 102 | background: Color = Color.Transparent, 103 | indication: Indication? = ActionButtonIndication, 104 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 105 | shape: Shape = RoundedCornerShape(3.dp), 106 | border: BorderStroke? = null, 107 | contentPadding: PaddingValues = ActionButtonDefaults.ContentPadding, 108 | content: @Composable RowScope.() -> Unit, 109 | ) { 110 | Box( 111 | modifier 112 | .then(if (border != null) Modifier.border(border, shape) else Modifier) 113 | .background( 114 | color = background, 115 | shape = shape 116 | ) 117 | .clip(shape) 118 | .clickable( 119 | interactionSource = interactionSource, 120 | indication = indication, 121 | enabled = enabled, 122 | onClick = onClick 123 | ) 124 | .hoverable(interactionSource, enabled), 125 | propagateMinConstraints = true 126 | ) { 127 | CompositionLocalProvider( 128 | LocalContentColor provides (if (!enabled) JBTheme.iconColors.disabled else LocalContentColor.current) 129 | ) { 130 | Row( 131 | Modifier 132 | .defaultMinSize( 133 | minWidth = ActionButtonDefaults.MinWidth, 134 | minHeight = ActionButtonDefaults.MinHeight 135 | ) 136 | .padding(contentPadding), 137 | horizontalArrangement = Arrangement.Center, 138 | verticalAlignment = Alignment.CenterVertically, 139 | content = content 140 | ) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/CheckBox.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.requiredSize 6 | import androidx.compose.foundation.layout.wrapContentSize 7 | import androidx.compose.foundation.selection.triStateToggleable 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.geometry.CornerRadius 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.geometry.Size 15 | import androidx.compose.ui.graphics.BlendMode 16 | import androidx.compose.ui.graphics.ColorFilter 17 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 18 | import androidx.compose.ui.semantics.Role 19 | import androidx.compose.ui.state.ToggleableState 20 | import androidx.compose.ui.unit.dp 21 | import io.kanro.compose.jetbrains.JBTheme 22 | import io.kanro.compose.jetbrains.icons.JBIcons 23 | import io.kanro.compose.jetbrains.icons.jbicons.Actions 24 | import io.kanro.compose.jetbrains.icons.jbicons.actions.Checkmark 25 | import io.kanro.compose.jetbrains.icons.jbicons.actions.CheckmarkIndeterminate 26 | 27 | @Composable 28 | fun CheckBox( 29 | checked: Boolean, 30 | onCheckedChange: ((Boolean) -> Unit)?, 31 | modifier: Modifier = Modifier, 32 | enabled: Boolean = true, 33 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 34 | ) { 35 | TriStateCheckbox( 36 | state = ToggleableState(checked), 37 | onClick = if (onCheckedChange != null) { 38 | { onCheckedChange(!checked) } 39 | } else null, 40 | interactionSource = interactionSource, 41 | enabled = enabled, 42 | modifier = modifier 43 | ) 44 | } 45 | 46 | @Composable 47 | fun TriStateCheckbox( 48 | state: ToggleableState, 49 | onClick: (() -> Unit)?, 50 | modifier: Modifier = Modifier, 51 | enabled: Boolean = true, 52 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 53 | ) { 54 | val toggleableModifier = 55 | if (onClick != null) { 56 | Modifier.triStateToggleable( 57 | state = state, 58 | onClick = onClick, 59 | enabled = enabled, 60 | role = Role.Checkbox, 61 | interactionSource = interactionSource, 62 | indication = null 63 | ) 64 | } else { 65 | Modifier 66 | } 67 | 68 | CheckboxImpl( 69 | enabled = enabled, 70 | value = state, 71 | modifier = modifier 72 | .then(toggleableModifier), 73 | ) 74 | } 75 | 76 | @Composable 77 | private fun CheckboxImpl( 78 | enabled: Boolean, 79 | value: ToggleableState, 80 | modifier: Modifier, 81 | ) { 82 | val icon = when (value) { 83 | ToggleableState.On -> rememberVectorPainter(JBIcons.Actions.Checkmark) 84 | ToggleableState.Indeterminate -> rememberVectorPainter(JBIcons.Actions.CheckmarkIndeterminate) 85 | else -> null 86 | } 87 | val iconFilter = if (!enabled) { 88 | ColorFilter.tint(JBTheme.iconColors.disabled, BlendMode.DstIn) 89 | } else null 90 | 91 | val bg = if (enabled) { 92 | when (value) { 93 | ToggleableState.On, ToggleableState.Indeterminate -> JBTheme.checkBoxColors.bgSelected 94 | ToggleableState.Off -> JBTheme.checkBoxColors.bg 95 | } 96 | } else JBTheme.checkBoxColors.bgDisabled 97 | 98 | val border = if (enabled) { 99 | when (value) { 100 | ToggleableState.On, ToggleableState.Indeterminate -> JBTheme.checkBoxColors.borderSelected 101 | ToggleableState.Off -> JBTheme.checkBoxColors.border 102 | } 103 | } else JBTheme.checkBoxColors.borderDisabled 104 | 105 | Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(14.dp)) { 106 | drawRoundRect(border, cornerRadius = CornerRadius(2.dp.toPx())) 107 | drawRoundRect( 108 | bg, 109 | size = Size(12.dp.toPx(), 12.dp.toPx()), 110 | topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), 111 | cornerRadius = CornerRadius(1.dp.toPx()) 112 | ) 113 | if (icon != null) { 114 | with(icon) { 115 | 14.dp.toPx() 116 | draw(Size(14.dp.toPx(), 14.dp.toPx()), colorFilter = iconFilter) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ComboBox.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.focusable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.defaultMinSize 12 | import androidx.compose.foundation.layout.fillMaxHeight 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.layout.width 17 | import androidx.compose.foundation.shape.RoundedCornerShape 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.drawBehind 24 | import androidx.compose.ui.focus.FocusRequester 25 | import androidx.compose.ui.focus.focusRequester 26 | import androidx.compose.ui.geometry.RoundRect 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.graphics.Path 29 | import androidx.compose.ui.graphics.PathFillType 30 | import androidx.compose.ui.graphics.Shape 31 | import androidx.compose.ui.graphics.addOutline 32 | import androidx.compose.ui.unit.Dp 33 | import androidx.compose.ui.unit.DpOffset 34 | import androidx.compose.ui.unit.dp 35 | import io.kanro.compose.jetbrains.JBThemeStyle 36 | import io.kanro.compose.jetbrains.LocalIconTheme 37 | import io.kanro.compose.jetbrains.icons.JBIcons 38 | import io.kanro.compose.jetbrains.icons.jbicons.General 39 | import io.kanro.compose.jetbrains.icons.jbicons.general.ButtonDropTriangle 40 | import io.kanro.compose.jetbrains.icons.jbicons.general.ButtonDropTriangleDark 41 | 42 | @Composable 43 | fun DropdownList( 44 | items: List, 45 | value: T, 46 | onValueChange: ((T) -> Unit)? = null, 47 | modifier: Modifier = Modifier, 48 | enabled: Boolean = true, 49 | valueRender: (T) -> String = { it.toString() }, 50 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 51 | style: TextFieldStyle = TextFieldDefaults.textFieldStyle(), 52 | ) { 53 | val shape = RoundedCornerShape(3.dp) 54 | val focusRequester = remember { FocusRequester() } 55 | val expanded = remember { mutableStateOf(false) } 56 | 57 | Box( 58 | modifier.border(1.dp, style.borderColor(enabled, false, interactionSource).value, shape).height(24.dp) 59 | .background(style.backgroundColor(enabled, interactionSource).value).focusable(enabled, interactionSource) 60 | .focusRequester(focusRequester) 61 | .clickable(interactionSource = interactionSource, indication = null, enabled = enabled, onClick = { 62 | expanded.value = true 63 | focusRequester.requestFocus() 64 | }).comboBoxIndicator(style.indicatorColor(false, interactionSource).value, shape, 2.dp) 65 | ) { 66 | Row(verticalAlignment = Alignment.CenterVertically) { 67 | Spacer(Modifier.width(6.dp)) 68 | Text( 69 | valueRender(value), 70 | Modifier.defaultMinSize(50.dp), 71 | color = style.textColor(enabled, false, interactionSource).value 72 | ) 73 | Spacer(Modifier.width(1.dp).fillMaxHeight()) 74 | Box(Modifier.size(22.dp), contentAlignment = Alignment.Center) { 75 | val isDarkTheme = LocalIconTheme.current == JBThemeStyle.DARK 76 | Icon( 77 | imageVector = if (isDarkTheme) JBIcons.General.ButtonDropTriangleDark 78 | else JBIcons.General.ButtonDropTriangle 79 | ) 80 | } 81 | } 82 | 83 | DropdownMenu(expanded.value, offset = DpOffset(0.dp, 4.dp), onDismissRequest = { 84 | expanded.value = false 85 | }) { 86 | items.forEach { 87 | DropdownMenuItem( 88 | modifier = Modifier.defaultMinSize(79.dp, 21.dp), onClick = { 89 | expanded.value = false 90 | onValueChange?.invoke(it) 91 | }, enabled = enabled 92 | ) { 93 | Text(valueRender(it), Modifier.padding(horizontal = 6.dp)) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | private fun Modifier.comboBoxIndicator(color: Color, shape: Shape, width: Dp): Modifier { 101 | if (color.alpha == 0f) return this 102 | return drawBehind { 103 | val controlOutline = shape.createOutline(size, layoutDirection, this) 104 | val highlightPath = Path().apply { 105 | this.fillType = PathFillType.EvenOdd 106 | addOutline(controlOutline) 107 | val borderRect = controlOutline.bounds.inflate(width.toPx()) 108 | addRoundRect(RoundRect(borderRect, 4.dp.toPx(), 4.dp.toPx())) 109 | close() 110 | } 111 | drawPath(highlightPath, color) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ContentAlpha.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalContentAlpha = compositionLocalOf { 1f } 6 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ContentColor.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.compositionLocalOf 5 | import androidx.compose.ui.graphics.Color 6 | import io.kanro.compose.jetbrains.JBTheme 7 | 8 | val LocalContentColor = compositionLocalOf { Color.Unspecified } 9 | 10 | @Composable 11 | fun JBTheme.contentColorFor(backgroundColor: Color): Color { 12 | return if ((backgroundColor.green + backgroundColor.blue + backgroundColor.red) / 3.0 > 0.6) { 13 | textColors.default 14 | } else { 15 | textColors.white 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/EmbeddedTextField.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.interaction.InteractionSource 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.interaction.collectIsFocusedAsState 6 | import androidx.compose.foundation.text.KeyboardActions 7 | import androidx.compose.foundation.text.KeyboardOptions 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.State 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.rememberUpdatedState 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.drawBehind 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.Path 17 | import androidx.compose.ui.graphics.PathFillType 18 | import androidx.compose.ui.graphics.RectangleShape 19 | import androidx.compose.ui.graphics.Shape 20 | import androidx.compose.ui.graphics.addOutline 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.text.input.VisualTransformation 23 | import androidx.compose.ui.unit.Dp 24 | import androidx.compose.ui.unit.dp 25 | 26 | @Composable 27 | fun EmbeddedTextField( 28 | value: String, 29 | onValueChange: (String) -> Unit, 30 | modifier: Modifier = Modifier, 31 | enabled: Boolean = true, 32 | readOnly: Boolean = false, 33 | textStyle: TextStyle = LocalTextStyle.current, 34 | placeholder: @Composable (() -> Unit)? = null, 35 | leadingIcon: @Composable (() -> Unit)? = null, 36 | trailingIcon: @Composable (() -> Unit)? = null, 37 | isError: Boolean = false, 38 | visualTransformation: VisualTransformation = VisualTransformation.None, 39 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 40 | keyboardActions: KeyboardActions = KeyboardActions.Default, 41 | singleLine: Boolean = true, 42 | maxLines: Int = 1, 43 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 44 | shape: Shape = RectangleShape, 45 | style: TextFieldStyle = TextFieldDefaults.textFieldStyle(), 46 | ) { 47 | TextField( 48 | value, 49 | onValueChange, 50 | modifier.embeddedTextFieldIndicator(style.indicatorColor(isError, interactionSource).value, shape, 2.dp), 51 | enabled, 52 | readOnly, 53 | textStyle, 54 | placeholder, 55 | leadingIcon, 56 | trailingIcon, 57 | isError, 58 | visualTransformation, 59 | keyboardOptions, 60 | keyboardActions, 61 | singleLine, 62 | maxLines, 63 | interactionSource, 64 | shape, 65 | InnerStyle(style) 66 | ) 67 | } 68 | 69 | private class InnerStyle(val style: TextFieldStyle) : TextFieldStyle { 70 | @Composable 71 | override fun textColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State { 72 | val focused by interactionSource.collectIsFocusedAsState() 73 | 74 | return if (focused) style.textColor( 75 | enabled, 76 | isError, 77 | interactionSource 78 | ) else rememberUpdatedState(Color.Unspecified) 79 | } 80 | 81 | @Composable 82 | override fun backgroundColor(enabled: Boolean, interactionSource: InteractionSource): State { 83 | val focused by interactionSource.collectIsFocusedAsState() 84 | 85 | return if (focused) style.backgroundColor( 86 | enabled, 87 | interactionSource 88 | ) else rememberUpdatedState(Color.Transparent) 89 | } 90 | 91 | @Composable 92 | override fun placeholderColor(enabled: Boolean): State { 93 | return style.placeholderColor(enabled) 94 | } 95 | 96 | @Composable 97 | override fun borderColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State { 98 | return rememberUpdatedState(Color.Transparent) 99 | } 100 | 101 | @Composable 102 | override fun indicatorColor(isError: Boolean, interactionSource: InteractionSource): State { 103 | return rememberUpdatedState(Color.Transparent) 104 | } 105 | 106 | @Composable 107 | override fun cursorColor(isError: Boolean): State { 108 | return style.cursorColor(isError) 109 | } 110 | } 111 | 112 | private fun Modifier.embeddedTextFieldIndicator(color: Color, shape: Shape, width: Dp): Modifier { 113 | if (color.alpha == 0f) return this 114 | return drawBehind { 115 | val controlOutline = shape.createOutline(size, layoutDirection, this) 116 | val highlightPath = Path().apply { 117 | this.fillType = PathFillType.EvenOdd 118 | addOutline(controlOutline) 119 | addRect(controlOutline.bounds.deflate(width.toPx())) 120 | close() 121 | } 122 | drawPath(highlightPath, color) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Icon.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.draw.paint 9 | import androidx.compose.ui.geometry.Size 10 | import androidx.compose.ui.graphics.ColorFilter 11 | import androidx.compose.ui.graphics.ImageBitmap 12 | import androidx.compose.ui.graphics.isSpecified 13 | import androidx.compose.ui.graphics.painter.BitmapPainter 14 | import androidx.compose.ui.graphics.painter.Painter 15 | import androidx.compose.ui.graphics.toolingGraphicsLayer 16 | import androidx.compose.ui.graphics.vector.ImageVector 17 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 18 | import androidx.compose.ui.layout.ContentScale 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.semantics.Role 21 | import androidx.compose.ui.semantics.contentDescription 22 | import androidx.compose.ui.semantics.role 23 | import androidx.compose.ui.semantics.semantics 24 | import androidx.compose.ui.unit.dp 25 | import io.kanro.compose.jetbrains.JBThemeStyle 26 | import io.kanro.compose.jetbrains.LocalIconTheme 27 | 28 | @Composable 29 | fun Icon( 30 | resource: String, 31 | contentDescription: String? = null, 32 | modifier: Modifier = Modifier, 33 | colorFilter: ColorFilter? = null, 34 | ) { 35 | Icon( 36 | themedSvgResource(resource, LocalIconTheme.current), contentDescription, modifier, 37 | colorFilter = colorFilter, 38 | ) 39 | } 40 | 41 | @Composable 42 | fun Icon( 43 | bitmap: ImageBitmap, 44 | contentDescription: String? = null, 45 | modifier: Modifier = Modifier, 46 | colorFilter: ColorFilter? = null, 47 | ) { 48 | val painter = remember(bitmap) { BitmapPainter(bitmap) } 49 | Icon( 50 | painter = painter, 51 | contentDescription = contentDescription, 52 | modifier = modifier, 53 | colorFilter = colorFilter, 54 | ) 55 | } 56 | 57 | @Composable 58 | fun Icon( 59 | imageVector: ImageVector, 60 | contentDescription: String? = null, 61 | modifier: Modifier = Modifier, 62 | colorFilter: ColorFilter? = null, 63 | ) { 64 | Icon( 65 | painter = rememberVectorPainter(imageVector), 66 | contentDescription = contentDescription, 67 | modifier = modifier, 68 | colorFilter = colorFilter, 69 | ) 70 | } 71 | 72 | @Composable 73 | fun Icon( 74 | painter: Painter, 75 | contentDescription: String? = null, 76 | modifier: Modifier = Modifier, 77 | colorFilter: ColorFilter? = null, 78 | ) { 79 | val semantics = if (contentDescription != null) { 80 | Modifier.semantics { 81 | this.contentDescription = contentDescription 82 | this.role = Role.Image 83 | } 84 | } else { 85 | Modifier 86 | } 87 | val filter = colorFilter ?: run { 88 | if (LocalContentColor.current.isSpecified) { 89 | ColorFilter.tint(LocalContentColor.current.copy(alpha = LocalContentAlpha.current)) 90 | } else { 91 | null 92 | } 93 | } 94 | Box( 95 | modifier.toolingGraphicsLayer().defaultSizeFor(painter) 96 | .paint( 97 | painter, 98 | contentScale = ContentScale.None, 99 | colorFilter = filter 100 | ) 101 | .then(semantics) 102 | ) 103 | } 104 | 105 | @Composable 106 | fun themedSvgResource(resource: String, theme: JBThemeStyle = LocalIconTheme.current): Painter { 107 | var realResource = resource 108 | if (theme == JBThemeStyle.DARK) { 109 | if (!realResource.endsWith("_dark.svg")) { 110 | val dark = realResource.replace(".svg", "_dark.svg") 111 | if (Thread.currentThread().contextClassLoader.getResource(dark) != null) { 112 | realResource = dark 113 | } 114 | } 115 | } else { 116 | if (realResource.endsWith("_dark.svg")) { 117 | val light = realResource.replace("_dark.svg", ".svg") 118 | if (Thread.currentThread().contextClassLoader.getResource(light) != null) { 119 | realResource = light 120 | } 121 | } 122 | } 123 | return painterResource(realResource) 124 | } 125 | 126 | private fun Modifier.defaultSizeFor(painter: Painter) = 127 | this.then( 128 | if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) { 129 | DefaultIconSizeModifier 130 | } else { 131 | Modifier 132 | } 133 | ) 134 | 135 | private fun Size.isInfinite() = width.isInfinite() && height.isInfinite() 136 | 137 | private val DefaultIconSizeModifier = Modifier.size(16.dp) 138 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/JBToolBar.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.gestures.Orientation 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.CompositionLocalProvider 14 | import androidx.compose.runtime.compositionLocalOf 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import io.kanro.compose.jetbrains.JBTheme 20 | 21 | @Composable 22 | fun JBToolBar( 23 | orientation: Orientation, 24 | arrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(8.dp), 25 | modifier: Modifier = Modifier, 26 | content: @Composable () -> Unit, 27 | ) { 28 | CompositionLocalProvider( 29 | LocalToolBarOrientation provides orientation, 30 | ) { 31 | when (orientation) { 32 | Orientation.Vertical -> JBToolBarColumn( 33 | modifier.width(28.dp), 34 | verticalArrangement = arrangement, 35 | content = content 36 | ) 37 | 38 | Orientation.Horizontal -> JBToolBarRow( 39 | modifier.height(28.dp), 40 | horizontalArrangement = arrangement, 41 | content = content 42 | ) 43 | } 44 | } 45 | } 46 | 47 | @Composable 48 | private fun JBToolBarColumn( 49 | modifier: Modifier, 50 | verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), 51 | content: @Composable () -> Unit, 52 | ) { 53 | Column( 54 | modifier, 55 | verticalArrangement, 56 | Alignment.CenterHorizontally, 57 | ) { 58 | content() 59 | } 60 | } 61 | 62 | @Composable 63 | private fun JBToolBarRow( 64 | modifier: Modifier, 65 | horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), 66 | content: @Composable () -> Unit, 67 | ) { 68 | Row( 69 | modifier, 70 | horizontalArrangement, 71 | Alignment.CenterVertically, 72 | ) { 73 | content() 74 | } 75 | } 76 | 77 | val LocalToolBarOrientation = compositionLocalOf { Orientation.Horizontal } 78 | 79 | @Composable 80 | fun ToolBarSeparator(modifier: Modifier = Modifier, color: Color = JBTheme.toolBarColors.iconSplitBorder) { 81 | val orientation = LocalToolBarOrientation.current 82 | Spacer( 83 | modifier = modifier.run { 84 | when (orientation) { 85 | Orientation.Vertical -> size(20.dp, 1.dp) 86 | Orientation.Horizontal -> size(1.dp, 20.dp) 87 | } 88 | }.background(color) 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/JBTree.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.combinedClickable 7 | import androidx.compose.foundation.hoverable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.CompositionLocalProvider 17 | import androidx.compose.runtime.ProvidableCompositionLocal 18 | import androidx.compose.runtime.compositionLocalOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.graphicsLayer 23 | import androidx.compose.ui.unit.dp 24 | import io.kanro.compose.jetbrains.JBTheme 25 | import io.kanro.compose.jetbrains.JBThemeStyle 26 | import io.kanro.compose.jetbrains.LocalIconTheme 27 | import io.kanro.compose.jetbrains.icons.JBIcons 28 | import io.kanro.compose.jetbrains.icons.jbicons.Actions 29 | import io.kanro.compose.jetbrains.icons.jbicons.actions.ArrowExpand 30 | import io.kanro.compose.jetbrains.icons.jbicons.actions.ArrowExpandDark 31 | 32 | @Composable 33 | fun JBTreeItem( 34 | modifier: Modifier = Modifier, 35 | selected: Boolean, 36 | onClick: () -> Unit, 37 | content: @Composable () -> Unit, 38 | ) { 39 | JBTreeBasicItem(modifier, selected, onClick) { 40 | Box(Modifier.padding(start = 16.dp)) { 41 | content() 42 | } 43 | } 44 | } 45 | 46 | @Composable 47 | fun JBTreeItem( 48 | modifier: Modifier = Modifier, 49 | selected: Boolean, 50 | onClick: () -> Unit, 51 | expanded: Boolean, 52 | expanding: (Boolean) -> Unit, 53 | content: @Composable () -> Unit, 54 | children: @Composable () -> Unit, 55 | ) { 56 | Column(modifier) { 57 | JBTreeBasicItem( 58 | modifier, selected, onClick = onClick, 59 | onDoubleClick = { 60 | expanding(!expanded) 61 | onClick() 62 | } 63 | ) { 64 | Row { 65 | val isDarkTheme = LocalIconTheme.current == JBThemeStyle.DARK 66 | Icon( 67 | imageVector = if (isDarkTheme) JBIcons.Actions.ArrowExpandDark else JBIcons.Actions.ArrowExpand, 68 | modifier = Modifier.size(16.dp).clickable( 69 | interactionSource = remember { MutableInteractionSource() }, 70 | indication = null 71 | ) { 72 | expanding(!expanded) 73 | onClick() 74 | }.graphicsLayer(rotationZ = if (expanded) 90f else 0f).align(Alignment.CenterVertically) 75 | ) 76 | Box { 77 | content() 78 | } 79 | } 80 | } 81 | if (expanded) { 82 | CompositionLocalProvider( 83 | LocalTreeLevel provides LocalTreeLevel.current + 1, 84 | content = children 85 | ) 86 | } 87 | } 88 | } 89 | 90 | @Composable 91 | @OptIn(ExperimentalFoundationApi::class) 92 | internal fun JBTreeBasicItem( 93 | modifier: Modifier = Modifier, 94 | selected: Boolean = false, 95 | onClick: () -> Unit, 96 | onDoubleClick: (() -> Unit)? = null, 97 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 98 | content: @Composable () -> Unit, 99 | ) { 100 | val padding = 7 + (LocalTreeLevel.current - 1) * 18 101 | Box( 102 | Modifier.combinedClickable( 103 | interactionSource, 104 | indication = ListItemHoverIndication, 105 | onDoubleClick = onDoubleClick, 106 | onClick = onClick 107 | ).then(modifier) 108 | .height(20.dp) 109 | .run { 110 | if (selected) { 111 | background(color = JBTheme.selectionColors.active) 112 | } else { 113 | this 114 | } 115 | }.hoverable(interactionSource), 116 | ) { 117 | SelectionScope(selected) { 118 | Box(Modifier.padding(start = padding.dp)) { 119 | content() 120 | } 121 | } 122 | } 123 | } 124 | 125 | @Composable 126 | fun JBTreeList( 127 | modifier: Modifier = Modifier, 128 | content: @Composable () -> Unit, 129 | ) { 130 | Column(modifier) { 131 | content() 132 | } 133 | } 134 | 135 | val LocalTreeLevel: ProvidableCompositionLocal = compositionLocalOf { 136 | 1 137 | } 138 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/JPanel.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.BoxScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.drawWithCache 10 | import androidx.compose.ui.geometry.Offset 11 | import androidx.compose.ui.geometry.Rect 12 | import androidx.compose.ui.geometry.Size 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | import io.kanro.compose.jetbrains.JBTheme 17 | 18 | @Composable 19 | fun JPanel( 20 | modifier: Modifier = Modifier, 21 | content: @Composable BoxScope.() -> Unit, 22 | ) { 23 | Box(modifier.background(JBTheme.panelColors.bgDialog)) { 24 | content() 25 | } 26 | } 27 | 28 | @Composable 29 | fun JPanelBorder(modifier: Modifier = Modifier) { 30 | Spacer(modifier.background(JBTheme.panelColors.border)) 31 | } 32 | 33 | fun Modifier.jBorder(all: Dp = 0.dp, color: Color): Modifier { 34 | return jBorder(all, all, all, all, color) 35 | } 36 | 37 | fun Modifier.jBorder( 38 | horizontal: Dp = 0.dp, 39 | vertical: Dp = 0.dp, 40 | color: Color, 41 | ): Modifier { 42 | return jBorder(horizontal, horizontal, vertical, vertical, color) 43 | } 44 | 45 | fun Modifier.jBorder( 46 | start: Dp = 0.dp, 47 | end: Dp = 0.dp, 48 | top: Dp = 0.dp, 49 | bottom: Dp = 0.dp, 50 | color: Color, 51 | ): Modifier { 52 | return drawWithCache { 53 | onDrawWithContent { 54 | drawContent() 55 | var rect = Rect(Offset.Zero, size) 56 | 57 | if (start.roundToPx() > 0) { 58 | drawRect(color, rect.topLeft, Size(start.toPx(), rect.height)) 59 | rect = Rect(rect.left + start.roundToPx(), rect.top, rect.right, rect.bottom) 60 | } 61 | 62 | if (end.roundToPx() > 0) { 63 | drawRect(color, Offset(rect.right - end.toPx(), rect.top), Size(end.toPx(), rect.height)) 64 | rect = Rect(rect.left, rect.top, rect.right - end.roundToPx(), rect.bottom) 65 | } 66 | 67 | if (top.roundToPx() > 0) { 68 | drawRect(color, rect.topLeft, Size(rect.width, top.toPx())) 69 | rect = Rect(rect.left, rect.top + top.roundToPx(), rect.right, rect.bottom) 70 | } 71 | 72 | if (bottom.roundToPx() > 0) { 73 | drawRect(color, Offset(rect.left, rect.bottom - bottom.toPx()), Size(rect.width, bottom.toPx())) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ListView.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.IndicationInstance 5 | import androidx.compose.foundation.interaction.InteractionSource 6 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.State 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 12 | import io.kanro.compose.jetbrains.JBTheme 13 | 14 | object ListItemHoverIndication : Indication { 15 | private class HoverIndicationInstance( 16 | private val isHover: State, 17 | private val hoverColor: Color, 18 | ) : IndicationInstance { 19 | override fun ContentDrawScope.drawIndication() { 20 | if (isHover.value) { 21 | drawRect(hoverColor, size = size) 22 | } 23 | drawContent() 24 | } 25 | } 26 | 27 | @Composable 28 | override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { 29 | val isHover = interactionSource.collectIsHoveredAsState() 30 | val hoverColor = JBTheme.selectionColors.hover 31 | 32 | return remember(JBTheme.selectionColors, interactionSource) { 33 | HoverIndicationInstance( 34 | isHover, hoverColor 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ProgressBar.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.animation.core.animateFloat 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.keyframes 6 | import androidx.compose.animation.core.rememberInfiniteTransition 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.focusable 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.progressSemantics 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.geometry.Offset 15 | import androidx.compose.ui.graphics.Brush 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.StrokeCap 18 | import androidx.compose.ui.graphics.TileMode 19 | import androidx.compose.ui.unit.dp 20 | import io.kanro.compose.jetbrains.JBTheme 21 | 22 | @Composable 23 | fun ProgressBar( 24 | progress: Float, 25 | modifier: Modifier = Modifier, 26 | ) { 27 | val bgColor = JBTheme.progressColors.bg 28 | val progressColor = JBTheme.progressColors.progress 29 | Canvas( 30 | modifier 31 | .progressSemantics(progress) 32 | .size(200.dp, 4.dp) 33 | .focusable() 34 | ) { 35 | val strokeWidth = size.height 36 | val length = size.width 37 | 38 | drawLine( 39 | bgColor, 40 | Offset(0f, strokeWidth / 2f), 41 | Offset(length, strokeWidth / 2f), 42 | strokeWidth, 43 | cap = StrokeCap.Round 44 | ) 45 | drawLine( 46 | progressColor, 47 | Offset(0f, strokeWidth / 2f), 48 | Offset(length * progress, strokeWidth / 2f), 49 | strokeWidth, 50 | cap = StrokeCap.Round 51 | ) 52 | } 53 | } 54 | 55 | @Composable 56 | fun ProgressBar( 57 | modifier: Modifier = Modifier, 58 | ) { 59 | val transition = rememberInfiniteTransition() 60 | val currentOffset by transition.animateFloat( 61 | 0f, 62 | 1f, 63 | infiniteRepeatable( 64 | animation = keyframes { 65 | durationMillis = 1000 66 | } 67 | ) 68 | ) 69 | val progressColor = JBTheme.progressColors.progress 70 | 71 | Canvas( 72 | modifier 73 | .progressSemantics() 74 | .size(200.dp, 4.dp) 75 | .focusable() 76 | ) { 77 | val strokeWidth = size.height 78 | val length = size.width 79 | val offset = currentOffset * 80f 80 | val brush = Brush.linearGradient( 81 | listOf( 82 | Color(0x00FFFFFF), Color(0x7FFFFFFF), Color(0x00FFFFFF) 83 | ), 84 | start = Offset(offset, 0f), 85 | end = Offset(offset + 80f, 0f), 86 | tileMode = TileMode.Repeated 87 | ) 88 | drawLine( 89 | progressColor, 90 | Offset(0f, strokeWidth / 2f), 91 | Offset(length, strokeWidth / 2f), 92 | strokeWidth, 93 | cap = StrokeCap.Round 94 | ) 95 | drawLine( 96 | brush, 97 | Offset(0f, strokeWidth / 2f), 98 | Offset(length, strokeWidth / 2f), 99 | strokeWidth, 100 | cap = StrokeCap.Round 101 | ) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Selection.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.selection.selectable 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.ProvidedValue 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.drawWithContent 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.semantics.Role 18 | import io.kanro.compose.jetbrains.JBTheme 19 | import io.kanro.compose.jetbrains.JBThemeStyle 20 | import io.kanro.compose.jetbrains.LocalIconTheme 21 | import io.kanro.compose.jetbrains.LocalSelectionScope 22 | import io.kanro.compose.jetbrains.color.LocalTextColors 23 | 24 | val emptySelectionScope = emptyArray>() 25 | 26 | val lightSelectionScope: @Composable () -> Array> = { 27 | arrayOf( 28 | LocalIconTheme provides JBThemeStyle.DARK, 29 | LocalTextColors provides JBTheme.textColors.copy( 30 | infoInput = Color.White 31 | ), 32 | LocalContentColor provides Color.White, 33 | LocalContentAlpha provides 1.0f, 34 | LocalContextMenuRepresentation provides JBContextMenuRepresentation( 35 | JBTheme.panelColors.bgContent, JBTheme.panelColors.border 36 | ) 37 | ) 38 | } 39 | 40 | val darkSelectionScope: @Composable () -> Array> = { 41 | arrayOf( 42 | LocalIconTheme provides JBThemeStyle.DARK, 43 | LocalTextColors provides JBTheme.textColors.copy( 44 | infoInput = Color.White 45 | ), 46 | LocalContentColor provides Color.White, 47 | LocalContentAlpha provides 1.0f, 48 | LocalContextMenuRepresentation provides JBContextMenuRepresentation( 49 | JBTheme.panelColors.bgContent, JBTheme.panelColors.border 50 | ) 51 | ) 52 | } 53 | 54 | @Composable 55 | fun SelectionScope(selected: Boolean, block: @Composable () -> Unit) { 56 | CompositionLocalProvider(* if (selected) LocalSelectionScope.current() else emptySelectionScope) { 57 | block() 58 | } 59 | } 60 | 61 | @Composable 62 | fun SelectionRow( 63 | selected: Boolean, 64 | modifier: Modifier = Modifier, 65 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 66 | verticalAlignment: Alignment.Vertical = Alignment.Top, 67 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 68 | role: Role? = null, 69 | onClick: () -> Unit, 70 | content: @Composable RowScope.() -> Unit, 71 | ) { 72 | SelectionScope(selected) { 73 | val selectedColor = JBTheme.selectionColors.active 74 | Row( 75 | modifier = modifier.background(color = JBTheme.panelColors.bgContent).selectable( 76 | selected = selected, 77 | interactionSource = interactionSource, 78 | indication = ListItemHoverIndication, 79 | onClick = onClick, 80 | role = role 81 | ).drawWithContent { 82 | if (selected) { 83 | drawRect(selectedColor, size = size) 84 | } 85 | drawContent() 86 | }, 87 | horizontalArrangement = horizontalArrangement, verticalAlignment = verticalAlignment, content = content 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Tab.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.IndicationInstance 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.hoverable 7 | import androidx.compose.foundation.interaction.InteractionSource 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.BoxScope 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.selection.selectable 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.State 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.drawWithContent 20 | import androidx.compose.ui.geometry.Offset 21 | import androidx.compose.ui.geometry.Size 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 24 | import androidx.compose.ui.semantics.Role 25 | import androidx.compose.ui.unit.dp 26 | import io.kanro.compose.jetbrains.JBTheme 27 | 28 | object TabIndication : Indication { 29 | private class TabIndicationInstance( 30 | private val isHover: State, 31 | private val hoverColor: Color, 32 | ) : IndicationInstance { 33 | override fun ContentDrawScope.drawIndication() { 34 | if (isHover.value) { 35 | drawRect(hoverColor, Offset.Zero, size) 36 | } 37 | drawContent() 38 | } 39 | } 40 | 41 | @Composable 42 | override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { 43 | val isHover = interactionSource.collectIsHoveredAsState() 44 | val hoverColor = JBTheme.tabColors.hover 45 | 46 | return remember(JBTheme.tabColors, interactionSource) { 47 | TabIndicationInstance( 48 | isHover, 49 | hoverColor, 50 | ) 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | fun Tab( 57 | selected: Boolean, 58 | onClick: () -> Unit, 59 | modifier: Modifier = Modifier, 60 | enabled: Boolean = true, 61 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 62 | content: @Composable BoxScope.() -> Unit, 63 | ) { 64 | val selectionColor = JBTheme.tabColors.selection 65 | Box( 66 | modifier 67 | .hoverable(interactionSource) 68 | .height(28.dp) 69 | .run { 70 | if (selected) { 71 | background(JBTheme.tabColors.bgSelected) 72 | } else { 73 | this 74 | } 75 | } 76 | .drawWithContent { 77 | drawContent() 78 | if (selected) { 79 | val height = 3.dp.toPx() 80 | drawRect( 81 | selectionColor, 82 | Offset(0f, size.height - height), 83 | Size(size.width, height) 84 | ) 85 | } 86 | }.selectable( 87 | selected = selected, 88 | onClick = onClick, 89 | enabled = enabled, 90 | role = Role.Tab, 91 | interactionSource = interactionSource, 92 | indication = TabIndication 93 | ), 94 | propagateMinConstraints = true, 95 | contentAlignment = Alignment.Center, 96 | content = content 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Text.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.control 2 | 3 | import androidx.compose.foundation.text.BasicText 4 | import androidx.compose.foundation.text.InlineTextContent 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.compositionLocalOf 8 | import androidx.compose.runtime.structuralEqualityPolicy 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.takeOrElse 12 | import androidx.compose.ui.text.AnnotatedString 13 | import androidx.compose.ui.text.TextLayoutResult 14 | import androidx.compose.ui.text.TextStyle 15 | import androidx.compose.ui.text.font.FontFamily 16 | import androidx.compose.ui.text.font.FontStyle 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.text.style.TextDecoration 20 | import androidx.compose.ui.text.style.TextOverflow 21 | import androidx.compose.ui.unit.TextUnit 22 | import io.kanro.compose.jetbrains.color.LocalTextColors 23 | import io.kanro.compose.jetbrains.color.TextColors 24 | 25 | @Composable 26 | fun Text( 27 | text: String, 28 | modifier: Modifier = Modifier, 29 | color: Color = Color.Unspecified, 30 | fontSize: TextUnit = TextUnit.Unspecified, 31 | fontStyle: FontStyle? = null, 32 | fontWeight: FontWeight? = null, 33 | fontFamily: FontFamily? = null, 34 | letterSpacing: TextUnit = TextUnit.Unspecified, 35 | textDecoration: TextDecoration? = null, 36 | textAlign: TextAlign? = null, 37 | lineHeight: TextUnit = TextUnit.Unspecified, 38 | overflow: TextOverflow = TextOverflow.Clip, 39 | softWrap: Boolean = true, 40 | maxLines: Int = Int.MAX_VALUE, 41 | onTextLayout: (TextLayoutResult) -> Unit = {}, 42 | style: TextStyle = LocalTextStyle.current, 43 | textColors: TextColors = LocalTextColors.current, 44 | ) { 45 | Text( 46 | AnnotatedString(text), 47 | modifier, 48 | color, 49 | fontSize, 50 | fontStyle, 51 | fontWeight, 52 | fontFamily, 53 | letterSpacing, 54 | textDecoration, 55 | textAlign, 56 | lineHeight, 57 | overflow, 58 | softWrap, 59 | maxLines, 60 | emptyMap(), 61 | onTextLayout, 62 | style, 63 | textColors, 64 | ) 65 | } 66 | 67 | @Composable 68 | fun Text( 69 | text: AnnotatedString, 70 | modifier: Modifier = Modifier, 71 | color: Color = Color.Unspecified, 72 | fontSize: TextUnit = TextUnit.Unspecified, 73 | fontStyle: FontStyle? = null, 74 | fontWeight: FontWeight? = null, 75 | fontFamily: FontFamily? = null, 76 | letterSpacing: TextUnit = TextUnit.Unspecified, 77 | textDecoration: TextDecoration? = null, 78 | textAlign: TextAlign? = null, 79 | lineHeight: TextUnit = TextUnit.Unspecified, 80 | overflow: TextOverflow = TextOverflow.Clip, 81 | softWrap: Boolean = true, 82 | maxLines: Int = Int.MAX_VALUE, 83 | inlineContent: Map = mapOf(), 84 | onTextLayout: (TextLayoutResult) -> Unit = {}, 85 | style: TextStyle = LocalTextStyle.current, 86 | textColors: TextColors = LocalTextColors.current, 87 | ) { 88 | val textColor = color.takeOrElse { 89 | style.color.takeOrElse { 90 | LocalContentColor.current.takeOrElse { 91 | textColors.default 92 | }.copy(alpha = LocalContentAlpha.current) 93 | } 94 | } 95 | val mergedStyle = style.merge( 96 | TextStyle( 97 | color = textColor, 98 | fontSize = fontSize, 99 | fontWeight = fontWeight, 100 | textAlign = textAlign, 101 | lineHeight = lineHeight, 102 | fontFamily = fontFamily, 103 | textDecoration = textDecoration, 104 | fontStyle = fontStyle, 105 | letterSpacing = letterSpacing, 106 | ), 107 | ) 108 | BasicText( 109 | text = text, 110 | modifier = modifier, 111 | style = mergedStyle, 112 | onTextLayout = onTextLayout, 113 | overflow = overflow, 114 | softWrap = softWrap, 115 | maxLines = maxLines, 116 | minLines = 1, 117 | inlineContent = inlineContent, 118 | ) 119 | } 120 | 121 | val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default } 122 | 123 | @Composable 124 | fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) { 125 | val mergedStyle = LocalTextStyle.current.merge(value) 126 | CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content) 127 | } 128 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/__JBIcons.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons 2 | 3 | internal object JBIcons 4 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/__Actions.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons 2 | 3 | import io.kanro.compose.jetbrains.icons.JBIcons 4 | 5 | internal object ActionsGroup 6 | 7 | internal val JBIcons.Actions: ActionsGroup 8 | get() = ActionsGroup 9 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/__General.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons 2 | 3 | import io.kanro.compose.jetbrains.icons.JBIcons 4 | 5 | internal object GeneralGroup 6 | 7 | internal val JBIcons.General: GeneralGroup 8 | get() = GeneralGroup 9 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/ArrowExpand.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.actions 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup 13 | 14 | internal val ActionsGroup.ArrowExpand: ImageVector 15 | get() { 16 | if (_arrowExpand != null) { 17 | return _arrowExpand!! 18 | } 19 | _arrowExpand = Builder( 20 | name = "ArrowExpand", defaultWidth = 16.0.dp, 21 | defaultHeight = 22 | 16.0.dp, 23 | viewportWidth = 16.0f, viewportHeight = 16.0f 24 | ).apply { 25 | path( 26 | fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF6E6E6E)), 27 | strokeLineWidth = 2.0f, strokeLineCap = Butt, strokeLineJoin = Miter, 28 | strokeLineMiter = 4.0f, pathFillType = NonZero 29 | ) { 30 | moveTo(6.0f, 13.0f) 31 | lineTo(11.0f, 8.0f) 32 | lineTo(6.0f, 3.0f) 33 | } 34 | } 35 | .build() 36 | return _arrowExpand!! 37 | } 38 | 39 | private var _arrowExpand: ImageVector? = null 40 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/ArrowExpandDark.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.actions 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup 13 | 14 | internal val ActionsGroup.ArrowExpandDark: ImageVector 15 | get() { 16 | if (_arrowExpandDark != null) { 17 | return _arrowExpandDark!! 18 | } 19 | _arrowExpandDark = Builder( 20 | name = "ArrowexpandDark", defaultWidth = 16.0.dp, 21 | defaultHeight = 22 | 16.0.dp, 23 | viewportWidth = 16.0f, viewportHeight = 16.0f 24 | ).apply { 25 | path( 26 | fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFFAFB1B3)), 27 | strokeLineWidth = 2.0f, strokeLineCap = Butt, strokeLineJoin = Miter, 28 | strokeLineMiter = 4.0f, pathFillType = NonZero 29 | ) { 30 | moveTo(6.0f, 13.0f) 31 | lineTo(11.0f, 8.0f) 32 | lineTo(6.0f, 3.0f) 33 | } 34 | } 35 | .build() 36 | return _arrowExpandDark!! 37 | } 38 | 39 | private var _arrowExpandDark: ImageVector? = null 40 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/Checkmark.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.actions 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup 13 | 14 | internal val ActionsGroup.Checkmark: ImageVector 15 | get() { 16 | if (_checkmark != null) { 17 | return _checkmark!! 18 | } 19 | _checkmark = Builder( 20 | name = "Checkmark", defaultWidth = 14.0.dp, defaultHeight = 14.0.dp, 21 | viewportWidth = 14.0f, viewportHeight = 14.0f 22 | ).apply { 23 | path( 24 | fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, 25 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 26 | pathFillType = EvenOdd 27 | ) { 28 | moveTo(5.625f, 8.4267f) 29 | lineTo(9.5566f, 2.9336f) 30 | curveTo(9.5566f, 2.9336f, 10.1737f, 2.3242f, 10.8612f, 2.8242f) 31 | curveTo(11.4433f, 3.3867f, 10.998f, 4.0938f, 10.998f, 4.0938f) 32 | lineTo(6.3183f, 10.6445f) 33 | curveTo(6.3183f, 10.6445f, 5.9941f, 11.0f, 5.5839f, 11.0f) 34 | curveTo(5.1737f, 11.0f, 4.873f, 10.6445f, 4.873f, 10.6445f) 35 | lineTo(2.9394f, 7.7461f) 36 | curveTo(2.9394f, 7.7461f, 2.5683f, 6.9805f, 3.2558f, 6.4609f) 37 | curveTo(4.0605f, 6.0781f, 4.5605f, 6.8394f, 4.5605f, 6.8394f) 38 | lineTo(5.625f, 8.4267f) 39 | close() 40 | } 41 | } 42 | .build() 43 | return _checkmark!! 44 | } 45 | 46 | private var _checkmark: ImageVector? = null 47 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/CheckmarkIndeterminate.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.actions 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup 13 | 14 | internal val ActionsGroup.CheckmarkIndeterminate: ImageVector 15 | get() { 16 | if (_checkmarkIndeterminate != null) { 17 | return _checkmarkIndeterminate!! 18 | } 19 | _checkmarkIndeterminate = Builder( 20 | name = "CheckmarkIndeterminate", defaultWidth = 14.0.dp, 21 | defaultHeight = 14.0.dp, viewportWidth = 14.0f, viewportHeight = 14.0f 22 | ).apply { 23 | path( 24 | fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, 25 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 26 | pathFillType = NonZero 27 | ) { 28 | moveTo(3.7402f, 5.73f) 29 | lineTo(10.1402f, 5.73f) 30 | arcTo(1.0f, 1.0f, 0.0f, false, true, 11.1402f, 6.73f) 31 | lineTo(11.1402f, 7.23f) 32 | arcTo(1.0f, 1.0f, 0.0f, false, true, 10.1402f, 8.23f) 33 | lineTo(3.7402f, 8.23f) 34 | arcTo(1.0f, 1.0f, 0.0f, false, true, 2.7402f, 7.23f) 35 | lineTo(2.7402f, 6.73f) 36 | arcTo(1.0f, 1.0f, 0.0f, false, true, 3.7402f, 5.73f) 37 | close() 38 | } 39 | } 40 | .build() 41 | return _checkmarkIndeterminate!! 42 | } 43 | 44 | private var _checkmarkIndeterminate: ImageVector? = null 45 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/Close.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.actions 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup 13 | 14 | internal val ActionsGroup.Close: ImageVector 15 | get() { 16 | if (_close != null) { 17 | return _close!! 18 | } 19 | _close = Builder( 20 | name = "Close", defaultWidth = 16.0.dp, defaultHeight = 16.0.dp, 21 | viewportWidth = 16.0f, viewportHeight = 16.0f 22 | ).apply { 23 | path( 24 | fill = SolidColor(Color(0xFF7F8B91)), stroke = null, fillAlpha = 0.5f, 25 | strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, 26 | strokeLineMiter = 4.0f, pathFillType = EvenOdd 27 | ) { 28 | moveTo(7.9949f, 8.7051f) 29 | lineTo(4.8541f, 11.8541f) 30 | lineTo(4.147f, 11.147f) 31 | lineTo(7.2949f, 8.0051f) 32 | lineTo(4.147f, 4.8571f) 33 | lineTo(4.8541f, 4.15f) 34 | lineTo(8.002f, 7.298f) 35 | lineTo(11.144f, 4.15f) 36 | lineTo(11.8511f, 4.8571f) 37 | lineTo(8.702f, 7.998f) 38 | lineTo(11.8511f, 11.147f) 39 | lineTo(11.144f, 11.8541f) 40 | lineTo(7.9949f, 8.7051f) 41 | close() 42 | } 43 | } 44 | .build() 45 | return _close!! 46 | } 47 | 48 | private var _close: ImageVector? = null 49 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/general/ButtonDropTriangle.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.general 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.GeneralGroup 13 | 14 | internal val GeneralGroup.ButtonDropTriangle: ImageVector 15 | get() { 16 | if (_buttonDropTriangle != null) { 17 | return _buttonDropTriangle!! 18 | } 19 | _buttonDropTriangle = Builder( 20 | name = "ButtonDropTriangle", defaultWidth = 8.0.dp, 21 | defaultHeight = 4.0.dp, viewportWidth = 8.0f, viewportHeight = 4.0f 22 | ).apply { 23 | path( 24 | fill = SolidColor(Color(0xFF6E6E6E)), stroke = null, strokeLineWidth = 0.0f, 25 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 26 | pathFillType = EvenOdd 27 | ) { 28 | moveTo(4.0f, 4.0f) 29 | lineToRelative(4.0f, -4.0f) 30 | lineToRelative(-8.0f, -0.0f) 31 | close() 32 | } 33 | } 34 | .build() 35 | return _buttonDropTriangle!! 36 | } 37 | 38 | private var _buttonDropTriangle: ImageVector? = null 39 | -------------------------------------------------------------------------------- /classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/general/ButtonDropTriangleDark.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.icons.jbicons.general 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import io.kanro.compose.jetbrains.icons.jbicons.GeneralGroup 13 | 14 | internal val GeneralGroup.ButtonDropTriangleDark: ImageVector 15 | get() { 16 | if (_buttonDropTriangleDark != null) { 17 | return _buttonDropTriangleDark!! 18 | } 19 | _buttonDropTriangleDark = Builder( 20 | name = "ButtonDropTriangleDark", defaultWidth = 8.0.dp, 21 | defaultHeight = 4.0.dp, viewportWidth = 8.0f, viewportHeight = 4.0f 22 | ).apply { 23 | path( 24 | fill = SolidColor(Color(0xFFAFB1B3)), stroke = null, strokeLineWidth = 0.0f, 25 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 26 | pathFillType = EvenOdd 27 | ) { 28 | moveTo(4.0f, 4.0f) 29 | lineToRelative(4.0f, -4.0f) 30 | lineToRelative(-8.0f, -0.0f) 31 | close() 32 | } 33 | } 34 | .build() 35 | return _buttonDropTriangleDark!! 36 | } 37 | 38 | private var _buttonDropTriangleDark: ImageVector? = null 39 | -------------------------------------------------------------------------------- /docs/gradle-jvm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/docs/gradle-jvm.png -------------------------------------------------------------------------------- /docs/project-sdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/docs/project-sdk.png -------------------------------------------------------------------------------- /docs/screenshot-expui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/docs/screenshot-expui.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/docs/screenshot.png -------------------------------------------------------------------------------- /expui-gallery/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("org.jetbrains.compose") 4 | } 5 | 6 | dependencies { 7 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") 8 | implementation(project(":compose-jetbrains-expui-theme")) 9 | implementation(compose.desktop.currentOs) { 10 | exclude("org.jetbrains.compose.material") 11 | } 12 | implementation(compose.uiTooling) 13 | } 14 | 15 | compose.desktop { 16 | application { 17 | mainClass = "MainKt" 18 | nativeDistributions { 19 | packageName = "JetBrains ExpUI Gallery" 20 | packageVersion = project.version.toString() 21 | copyright = "Beijing Muke Technology Co., Ltd." 22 | modules("jdk.unsupported") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/darkTheme.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/darkTheme_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/generic.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/generic_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/github_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/kotlin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/lightTheme.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/lightTheme_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/settings_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /expui-gallery/src/main/resources/icons/text_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /expui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("com.netflix.nebula.maven-publish") 4 | id("com.netflix.nebula.source-jar") 5 | id("com.bybutter.sisyphus.project") 6 | id("org.jetbrains.compose") 7 | `java-library` 8 | } 9 | 10 | description = "JetBrains ExpUI Kit for Compose Desktop" 11 | 12 | dependencies { 13 | implementation(kotlin("stdlib")) 14 | implementation(compose.desktop.common) { 15 | exclude("org.jetbrains.compose.material") 16 | } 17 | } 18 | 19 | tasks.withType() { 20 | kotlinOptions.jvmTarget = "17" 21 | } 22 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/DesktopPlatform.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui 2 | 3 | enum class DesktopPlatform { 4 | Linux, 5 | Windows, 6 | MacOS, 7 | Unknown; 8 | 9 | companion object { 10 | val Current: DesktopPlatform by lazy { 11 | val name = System.getProperty("os.name") 12 | when { 13 | name?.startsWith("Linux") == true -> Linux 14 | name?.startsWith("Win") == true -> Windows 15 | name == "Mac OS X" -> MacOS 16 | else -> Unknown 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ActionButton.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.BoxScope 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.compositionLocalOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.semantics.Role 16 | import androidx.compose.ui.unit.dp 17 | import io.kanro.compose.jetbrains.expui.style.AreaColors 18 | import io.kanro.compose.jetbrains.expui.style.AreaProvider 19 | import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider 20 | import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider 21 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 22 | import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors 23 | import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors 24 | import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors 25 | import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors 26 | import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider 27 | import io.kanro.compose.jetbrains.expui.style.areaBackground 28 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 29 | 30 | class ActionButtonColors( 31 | override val normalAreaColors: AreaColors, 32 | override val hoverAreaColors: AreaColors, 33 | override val pressedAreaColors: AreaColors, 34 | override val disabledAreaColors: AreaColors, 35 | ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, DisabledAreaProvider { 36 | @Composable 37 | fun provideArea(enabled: Boolean, content: @Composable () -> Unit) { 38 | CompositionLocalProvider( 39 | LocalAreaColors provides if (enabled) normalAreaColors else disabledAreaColors, 40 | LocalNormalAreaColors provides normalAreaColors, 41 | LocalDisabledAreaColors provides disabledAreaColors, 42 | LocalHoverAreaColors provides hoverAreaColors, 43 | LocalPressedAreaColors provides pressedAreaColors, 44 | content = content 45 | ) 46 | } 47 | } 48 | 49 | val LocalActionButtonColors = compositionLocalOf { 50 | LightTheme.ActionButtonColors 51 | } 52 | 53 | @Composable 54 | fun ActionButton( 55 | onClick: () -> Unit, 56 | modifier: Modifier = Modifier, 57 | enabled: Boolean = true, 58 | shape: Shape = RoundedCornerShape(6.dp), 59 | indication: Indication? = HoverOrPressedIndication(shape), 60 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 61 | colors: ActionButtonColors = LocalActionButtonColors.current, 62 | content: @Composable BoxScope.() -> Unit, 63 | ) { 64 | colors.provideArea(enabled) { 65 | Box( 66 | modifier.areaBackground(shape = shape).clickable( 67 | interactionSource = interactionSource, 68 | indication = indication, 69 | enabled = enabled, 70 | onClick = onClick, 71 | role = Role.Button 72 | ), 73 | propagateMinConstraints = true 74 | ) { 75 | content() 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ContextCompositionLocals.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | val LocalContentActivated = compositionLocalOf { true } 6 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Icon.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.Size 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.ColorFilter 11 | import androidx.compose.ui.graphics.ImageBitmap 12 | import androidx.compose.ui.graphics.isSpecified 13 | import androidx.compose.ui.graphics.painter.BitmapPainter 14 | import androidx.compose.ui.graphics.painter.Painter 15 | import androidx.compose.ui.graphics.toolingGraphicsLayer 16 | import androidx.compose.ui.graphics.vector.ImageVector 17 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 18 | import androidx.compose.ui.layout.ContentScale 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.semantics.Role 21 | import androidx.compose.ui.semantics.contentDescription 22 | import androidx.compose.ui.semantics.role 23 | import androidx.compose.ui.semantics.semantics 24 | import androidx.compose.ui.unit.dp 25 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 26 | import io.kanro.compose.jetbrains.expui.theme.LocalIsDarkTheme 27 | 28 | @Composable 29 | fun themedSvgResource(resource: String, isDark: Boolean = LocalIsDarkTheme.current): Painter { 30 | var realResource = resource 31 | if (isDark) { 32 | if (!realResource.endsWith("_dark.svg")) { 33 | val dark = realResource.replace(".svg", "_dark.svg") 34 | if (Thread.currentThread().contextClassLoader.getResource(dark) != null) { 35 | realResource = dark 36 | } 37 | } 38 | } else { 39 | if (realResource.endsWith("_dark.svg")) { 40 | val light = realResource.replace("_dark.svg", ".svg") 41 | if (Thread.currentThread().contextClassLoader.getResource(light) != null) { 42 | realResource = light 43 | } 44 | } 45 | } 46 | return painterResource(realResource) 47 | } 48 | 49 | private fun Modifier.defaultSizeFor(painter: Painter) = this.then( 50 | if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) { 51 | DefaultIconSizeModifier 52 | } else { 53 | Modifier 54 | } 55 | ) 56 | 57 | private fun Size.isInfinite() = width.isInfinite() && height.isInfinite() 58 | 59 | private val DefaultIconSizeModifier = Modifier.size(20.dp) 60 | 61 | @Composable 62 | fun Icon( 63 | resource: String, 64 | contentDescription: String? = null, 65 | modifier: Modifier = Modifier, 66 | colorFilter: ColorFilter? = null, 67 | markerColor: Color = Color.Unspecified, 68 | ) { 69 | Icon( 70 | themedSvgResource(resource), contentDescription, modifier, 71 | colorFilter = colorFilter, 72 | markerColor = markerColor, 73 | ) 74 | } 75 | 76 | @Composable 77 | fun Icon( 78 | bitmap: ImageBitmap, 79 | contentDescription: String? = null, 80 | modifier: Modifier = Modifier, 81 | colorFilter: ColorFilter? = null, 82 | markerColor: Color = Color.Unspecified, 83 | ) { 84 | val painter = remember(bitmap) { BitmapPainter(bitmap) } 85 | Icon( 86 | painter = painter, 87 | contentDescription = contentDescription, 88 | modifier = modifier, 89 | colorFilter = colorFilter, 90 | markerColor = markerColor, 91 | ) 92 | } 93 | 94 | @Composable 95 | fun Icon( 96 | imageVector: ImageVector, 97 | contentDescription: String? = null, 98 | modifier: Modifier = Modifier, 99 | colorFilter: ColorFilter? = null, 100 | markerColor: Color = Color.Unspecified, 101 | ) { 102 | Icon( 103 | painter = rememberVectorPainter(imageVector), 104 | contentDescription = contentDescription, 105 | modifier = modifier, 106 | colorFilter = colorFilter, 107 | markerColor = markerColor, 108 | ) 109 | } 110 | 111 | @Composable 112 | fun Icon( 113 | painter: Painter, 114 | contentDescription: String? = null, 115 | modifier: Modifier = Modifier, 116 | colorFilter: ColorFilter? = null, 117 | markerColor: Color = Color.Unspecified, 118 | ) { 119 | val semantics = if (contentDescription != null) { 120 | Modifier.semantics { 121 | this.contentDescription = contentDescription 122 | this.role = Role.Image 123 | } 124 | } else { 125 | Modifier 126 | } 127 | val filter = colorFilter ?: run { 128 | val foreground = LocalAreaColors.current.foreground 129 | if (foreground.isSpecified) { 130 | ColorFilter.tint(foreground) 131 | } else { 132 | null 133 | } 134 | } 135 | Box( 136 | modifier.toolingGraphicsLayer() 137 | .defaultSizeFor(painter) 138 | .paintWithMarker(painter, contentScale = ContentScale.None, colorFilter = filter, markerColor = markerColor) 139 | .then(semantics) 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Indication.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.interaction.InteractionSource 5 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 6 | import androidx.compose.foundation.interaction.collectIsPressedAsState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.State 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.Shape 12 | import androidx.compose.ui.graphics.drawOutline 13 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 14 | import androidx.compose.ui.graphics.isSpecified 15 | import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors 16 | import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors 17 | 18 | class HoverOrPressedIndication(private val shape: Shape) : Indication { 19 | private class IndicationInstance( 20 | private val shape: Shape, 21 | private val isHover: State, 22 | private val isPressed: State, 23 | private val hoverColor: Color, 24 | private val pressedColor: Color, 25 | ) : androidx.compose.foundation.IndicationInstance { 26 | override fun ContentDrawScope.drawIndication() { 27 | when { 28 | isPressed.value -> { 29 | if (pressedColor.isSpecified) { 30 | val outline = shape.createOutline(size, layoutDirection, this) 31 | drawOutline(outline, pressedColor) 32 | } 33 | } 34 | 35 | isHover.value -> { 36 | if (hoverColor.isSpecified) { 37 | val outline = shape.createOutline(size, layoutDirection, this) 38 | drawOutline(outline, hoverColor) 39 | } 40 | } 41 | } 42 | drawContent() 43 | } 44 | } 45 | 46 | @Composable 47 | override fun rememberUpdatedInstance(interactionSource: InteractionSource): androidx.compose.foundation.IndicationInstance { 48 | val hoverColors = LocalHoverAreaColors.current 49 | val pressedColors = LocalPressedAreaColors.current 50 | val isPressed = interactionSource.collectIsPressedAsState() 51 | val isHover = interactionSource.collectIsHoveredAsState() 52 | 53 | return remember(hoverColors, pressedColors, interactionSource) { 54 | IndicationInstance( 55 | shape, isHover, isPressed, hoverColors.startBackground, pressedColors.startBackground 56 | ) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Label.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.foundation.text.BasicText 4 | import androidx.compose.foundation.text.InlineTextContent 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.graphics.takeOrElse 9 | import androidx.compose.ui.text.AnnotatedString 10 | import androidx.compose.ui.text.TextLayoutResult 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.text.font.FontFamily 13 | import androidx.compose.ui.text.font.FontStyle 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.text.style.TextDecoration 17 | import androidx.compose.ui.text.style.TextOverflow 18 | import androidx.compose.ui.unit.TextUnit 19 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 20 | import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle 21 | 22 | @Composable 23 | fun Label( 24 | text: String, 25 | modifier: Modifier = Modifier, 26 | color: Color = Color.Unspecified, 27 | fontSize: TextUnit = TextUnit.Unspecified, 28 | fontStyle: FontStyle? = null, 29 | fontWeight: FontWeight? = null, 30 | fontFamily: FontFamily? = null, 31 | letterSpacing: TextUnit = TextUnit.Unspecified, 32 | textDecoration: TextDecoration? = null, 33 | textAlign: TextAlign? = null, 34 | lineHeight: TextUnit = TextUnit.Unspecified, 35 | overflow: TextOverflow = TextOverflow.Clip, 36 | softWrap: Boolean = true, 37 | maxLines: Int = Int.MAX_VALUE, 38 | onTextLayout: (TextLayoutResult) -> Unit = {}, 39 | style: TextStyle = LocalDefaultTextStyle.current, 40 | ) { 41 | val textColor = color.takeOrElse { 42 | style.color 43 | }.takeOrElse { 44 | LocalAreaColors.current.text 45 | } 46 | val mergedStyle = style.merge( 47 | TextStyle( 48 | color = textColor, 49 | fontSize = fontSize, 50 | fontWeight = fontWeight, 51 | textAlign = textAlign, 52 | lineHeight = lineHeight, 53 | fontFamily = fontFamily, 54 | textDecoration = textDecoration, 55 | fontStyle = fontStyle, 56 | letterSpacing = letterSpacing 57 | ) 58 | ) 59 | 60 | BasicText( 61 | text = text, 62 | modifier = modifier, 63 | style = mergedStyle, 64 | onTextLayout = onTextLayout, 65 | overflow = overflow, 66 | softWrap = softWrap, 67 | maxLines = maxLines, 68 | ) 69 | } 70 | 71 | @Composable 72 | fun Label( 73 | text: AnnotatedString, 74 | modifier: Modifier = Modifier, 75 | color: Color = Color.Unspecified, 76 | fontSize: TextUnit = TextUnit.Unspecified, 77 | fontStyle: FontStyle? = null, 78 | fontWeight: FontWeight? = null, 79 | fontFamily: FontFamily? = null, 80 | letterSpacing: TextUnit = TextUnit.Unspecified, 81 | textDecoration: TextDecoration? = null, 82 | textAlign: TextAlign? = null, 83 | lineHeight: TextUnit = TextUnit.Unspecified, 84 | overflow: TextOverflow = TextOverflow.Clip, 85 | softWrap: Boolean = true, 86 | maxLines: Int = Int.MAX_VALUE, 87 | inlineContent: Map = mapOf(), 88 | onTextLayout: (TextLayoutResult) -> Unit = {}, 89 | style: TextStyle = LocalDefaultTextStyle.current, 90 | ) { 91 | val textColor = color.takeOrElse { 92 | style.color 93 | }.takeOrElse { 94 | LocalAreaColors.current.text 95 | } 96 | val mergedStyle = style.merge( 97 | TextStyle( 98 | color = textColor, 99 | fontSize = fontSize, 100 | fontWeight = fontWeight, 101 | textAlign = textAlign, 102 | lineHeight = lineHeight, 103 | fontFamily = fontFamily, 104 | textDecoration = textDecoration, 105 | fontStyle = fontStyle, 106 | letterSpacing = letterSpacing 107 | ) 108 | ) 109 | 110 | BasicText( 111 | text = text, 112 | modifier = modifier, 113 | style = mergedStyle, 114 | onTextLayout = onTextLayout, 115 | overflow = overflow, 116 | softWrap = softWrap, 117 | maxLines = maxLines, 118 | inlineContent = inlineContent 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/PointerInput.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.input.pointer.PointerEventType 5 | import androidx.compose.ui.input.pointer.pointerInput 6 | 7 | fun Modifier.onHover(onHover: (Boolean) -> Unit): Modifier = this.pointerInput(Unit) { 8 | awaitPointerEventScope { 9 | while (true) { 10 | val event = awaitPointerEvent() 11 | when (event.type) { 12 | PointerEventType.Enter -> onHover(true) 13 | PointerEventType.Exit -> onHover(false) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ProgressBar.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.animation.core.animateFloat 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.keyframes 6 | import androidx.compose.animation.core.rememberInfiniteTransition 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.progressSemantics 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.compositionLocalOf 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.geometry.Offset 15 | import androidx.compose.ui.graphics.Brush 16 | import androidx.compose.ui.graphics.StrokeCap 17 | import androidx.compose.ui.graphics.TileMode 18 | import androidx.compose.ui.unit.dp 19 | import io.kanro.compose.jetbrains.expui.style.AreaColors 20 | import io.kanro.compose.jetbrains.expui.style.AreaProvider 21 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 22 | 23 | class ProgressBarColors( 24 | override val normalAreaColors: AreaColors, 25 | val indeterminateAreaColors: AreaColors, 26 | ) : AreaProvider 27 | 28 | val LocalProgressBarColors = compositionLocalOf { 29 | LightTheme.ProgressBarColors 30 | } 31 | 32 | @Composable 33 | fun ProgressBar( 34 | progress: Float, 35 | modifier: Modifier = Modifier, 36 | colors: ProgressBarColors = LocalProgressBarColors.current, 37 | ) { 38 | val currentColors = colors.normalAreaColors 39 | Canvas( 40 | modifier.progressSemantics(progress).size(200.dp, 4.dp) 41 | ) { 42 | val strokeWidth = size.height 43 | val length = size.width 44 | 45 | drawLine( 46 | currentColors.startBackground, 47 | Offset(0f, strokeWidth / 2f), 48 | Offset(length, strokeWidth / 2f), 49 | strokeWidth, 50 | cap = StrokeCap.Round 51 | ) 52 | drawLine( 53 | currentColors.foreground, 54 | Offset(0f, strokeWidth / 2f), 55 | Offset(length * progress, strokeWidth / 2f), 56 | strokeWidth, 57 | cap = StrokeCap.Round 58 | ) 59 | } 60 | } 61 | 62 | @Composable 63 | fun ProgressBar( 64 | modifier: Modifier = Modifier, 65 | colors: ProgressBarColors = LocalProgressBarColors.current, 66 | ) { 67 | val transition = rememberInfiniteTransition() 68 | val currentOffset by transition.animateFloat( 69 | 0f, 1f, 70 | infiniteRepeatable( 71 | animation = keyframes { 72 | durationMillis = 1000 73 | } 74 | ) 75 | ) 76 | val currentColors = colors.indeterminateAreaColors 77 | Canvas( 78 | modifier.progressSemantics().size(200.dp, 4.dp) 79 | ) { 80 | val strokeWidth = size.height 81 | val length = size.width 82 | val offset = currentOffset * length 83 | val brush = Brush.linearGradient( 84 | listOf(currentColors.startBackground, currentColors.endBackground, currentColors.startBackground), 85 | start = Offset(offset, 0f), 86 | end = Offset(offset + length, 0f), 87 | tileMode = TileMode.Repeated 88 | ) 89 | drawLine( 90 | brush, Offset(0f, strokeWidth / 2f), Offset(length, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ToolBar.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.BoxScope 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.compositionLocalOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.semantics.Role 16 | import androidx.compose.ui.unit.dp 17 | import io.kanro.compose.jetbrains.expui.style.AreaColors 18 | import io.kanro.compose.jetbrains.expui.style.AreaProvider 19 | import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider 20 | import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider 21 | import io.kanro.compose.jetbrains.expui.style.InactiveSelectionAreaProvider 22 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 23 | import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors 24 | import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors 25 | import io.kanro.compose.jetbrains.expui.style.LocalInactiveAreaColors 26 | import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors 27 | import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors 28 | import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors 29 | import io.kanro.compose.jetbrains.expui.style.LocalSelectionInactiveAreaColors 30 | import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider 31 | import io.kanro.compose.jetbrains.expui.style.areaBackground 32 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 33 | 34 | class ToolBarActionButtonColors( 35 | override val normalAreaColors: AreaColors, 36 | override val hoverAreaColors: AreaColors, 37 | override val pressedAreaColors: AreaColors, 38 | override val disabledAreaColors: AreaColors, 39 | override val selectionAreaColors: AreaColors, 40 | override val inactiveAreaColors: AreaColors, 41 | override val inactiveSelectionAreaColors: AreaColors, 42 | ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, DisabledAreaProvider, InactiveSelectionAreaProvider { 43 | @Composable 44 | fun provideArea(enabled: Boolean, selected: Boolean, content: @Composable () -> Unit) { 45 | val activated = LocalContentActivated.current 46 | val currentColors = when { 47 | !enabled -> disabledAreaColors 48 | selected -> if (activated) selectionAreaColors else inactiveSelectionAreaColors 49 | !activated -> inactiveAreaColors 50 | else -> normalAreaColors 51 | } 52 | CompositionLocalProvider( 53 | LocalAreaColors provides currentColors, 54 | LocalNormalAreaColors provides normalAreaColors, 55 | LocalDisabledAreaColors provides disabledAreaColors, 56 | LocalHoverAreaColors provides hoverAreaColors, 57 | LocalPressedAreaColors provides pressedAreaColors, 58 | LocalSelectionInactiveAreaColors provides inactiveSelectionAreaColors, 59 | LocalInactiveAreaColors provides inactiveAreaColors, 60 | LocalSelectionAreaColors provides selectionAreaColors, 61 | content = content 62 | ) 63 | } 64 | } 65 | 66 | val LocalToolBarActionButtonColors = compositionLocalOf { 67 | LightTheme.ToolBarActionButtonColors 68 | } 69 | 70 | @Composable 71 | fun ToolBarActionButton( 72 | selected: Boolean = false, 73 | onClick: () -> Unit, 74 | modifier: Modifier = Modifier, 75 | enabled: Boolean = true, 76 | shape: Shape = RoundedCornerShape(6.dp), 77 | indication: Indication? = HoverOrPressedIndication(shape), 78 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 79 | colors: ToolBarActionButtonColors = LocalToolBarActionButtonColors.current, 80 | content: @Composable BoxScope.() -> Unit, 81 | ) { 82 | colors.provideArea(enabled, selected) { 83 | Box( 84 | modifier.clickable( 85 | interactionSource = interactionSource, 86 | indication = indication, 87 | enabled = enabled, 88 | onClick = onClick, 89 | role = Role.Button 90 | ).areaBackground(shape = shape), 91 | propagateMinConstraints = true 92 | ) { 93 | content() 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Tooltip.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.control 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.TooltipArea 5 | import androidx.compose.foundation.TooltipPlacement 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.compositionLocalOf 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.shadow 15 | import androidx.compose.ui.unit.DpOffset 16 | import androidx.compose.ui.unit.dp 17 | import io.kanro.compose.jetbrains.expui.style.AreaColors 18 | import io.kanro.compose.jetbrains.expui.style.AreaProvider 19 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 20 | import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors 21 | import io.kanro.compose.jetbrains.expui.style.areaBackground 22 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 23 | import io.kanro.compose.jetbrains.expui.theme.LocalIsDarkTheme 24 | 25 | class ToolTipColors( 26 | val isDark: Boolean, 27 | override val normalAreaColors: AreaColors, 28 | ) : AreaProvider { 29 | @Composable 30 | fun provideArea(content: @Composable () -> Unit) { 31 | CompositionLocalProvider( 32 | LocalAreaColors provides normalAreaColors, 33 | LocalNormalAreaColors provides normalAreaColors, 34 | LocalIsDarkTheme provides isDark, 35 | content = content 36 | ) 37 | } 38 | } 39 | 40 | val LocalToolTipColors = compositionLocalOf { 41 | LightTheme.ToolTipColors 42 | } 43 | 44 | @Composable 45 | @OptIn(ExperimentalFoundationApi::class) 46 | fun Tooltip( 47 | tooltip: @Composable () -> Unit, 48 | modifier: Modifier = Modifier, 49 | delayMillis: Int = 500, 50 | tooltipPlacement: TooltipPlacement = TooltipPlacement.CursorPoint( 51 | offset = DpOffset(0.dp, 32.dp) 52 | ), 53 | colors: ToolTipColors = LocalToolTipColors.current, 54 | content: @Composable () -> Unit, 55 | ) { 56 | TooltipArea( 57 | { 58 | colors.provideArea { 59 | Box( 60 | modifier = Modifier.shadow(8.dp).areaBackground() 61 | .border(1.dp, LocalAreaColors.current.startBorderColor), 62 | contentAlignment = Alignment.Center 63 | ) { 64 | Box( 65 | modifier = Modifier.padding(16.dp), 66 | ) { 67 | tooltip() 68 | } 69 | } 70 | } 71 | }, modifier, delayMillis, tooltipPlacement, content 72 | ) 73 | } 74 | 75 | @Composable 76 | @OptIn(ExperimentalFoundationApi::class) 77 | fun Tooltip( 78 | tooltip: String, 79 | modifier: Modifier = Modifier, 80 | delayMillis: Int = 500, 81 | tooltipPlacement: TooltipPlacement = TooltipPlacement.CursorPoint( 82 | offset = DpOffset(0.dp, 32.dp) 83 | ), 84 | colors: ToolTipColors = LocalToolTipColors.current, 85 | content: @Composable () -> Unit, 86 | ) { 87 | Tooltip({ 88 | Label(tooltip) 89 | }, modifier, delayMillis, tooltipPlacement, colors, content) 90 | } 91 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/AreaColors.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.style 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.compositionLocalOf 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Brush 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.RectangleShape 11 | import androidx.compose.ui.graphics.Shape 12 | import androidx.compose.ui.graphics.isUnspecified 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 16 | 17 | /** 18 | * Color definition for an area which has background and foreground. 19 | */ 20 | data class AreaColors( 21 | /** 22 | * Text foreground color. 23 | */ 24 | val text: Color, 25 | /** 26 | * Overriding the foreground colour for some components that have their own colour like [Icon][io.kanro.compose.jetbrains.expui.control.Icon]. 27 | */ 28 | val foreground: Color, 29 | val startBackground: Color, 30 | val endBackground: Color, 31 | val startBorderColor: Color, 32 | val endBorderColor: Color, 33 | val focusColor: Color, 34 | ) 35 | 36 | @Composable 37 | fun Modifier.areaBackground(areaColors: AreaColors = LocalAreaColors.current, shape: Shape = RectangleShape): Modifier { 38 | return background(areaColors, shape) 39 | } 40 | 41 | fun Modifier.background(areaColors: AreaColors, shape: Shape = RectangleShape): Modifier { 42 | if (areaColors.startBackground.isUnspecified) { 43 | return this 44 | } 45 | if (areaColors.endBackground.isUnspecified || areaColors.startBackground == areaColors.endBackground) { 46 | return this.background(areaColors.startBackground, shape) 47 | } 48 | return this.background(Brush.linearGradient(listOf(areaColors.startBackground, areaColors.endBackground)), shape) 49 | } 50 | 51 | @Composable 52 | fun Modifier.areaBorder( 53 | areaColors: AreaColors = LocalAreaColors.current, 54 | width: Dp = 1.dp, 55 | shape: Shape = RectangleShape, 56 | ): Modifier { 57 | return border(areaColors, width, shape) 58 | } 59 | 60 | fun Modifier.border(areaColors: AreaColors, width: Dp = 1.dp, shape: Shape = RectangleShape): Modifier { 61 | if (areaColors.startBorderColor.isUnspecified) { 62 | return this 63 | } 64 | if (areaColors.endBorderColor.isUnspecified || areaColors.startBorderColor == areaColors.endBorderColor) { 65 | return this.border(width, areaColors.startBorderColor, shape) 66 | } 67 | return this.border(width, Brush.linearGradient(listOf(areaColors.startBackground, areaColors.endBackground)), shape) 68 | } 69 | 70 | @Composable 71 | fun Modifier.areaFocusBorder( 72 | focused: Boolean, 73 | areaColors: AreaColors = LocalAreaColors.current, 74 | width: Dp = 2.dp, 75 | shape: Shape = RectangleShape, 76 | ): Modifier { 77 | return focusBorder(focused, areaColors, width, shape) 78 | } 79 | 80 | fun Modifier.focusBorder( 81 | focused: Boolean, 82 | areaColors: AreaColors, 83 | width: Dp = 2.dp, 84 | shape: Shape = RectangleShape, 85 | ): Modifier { 86 | if (!focused) return this 87 | if (areaColors.focusColor.isUnspecified) { 88 | return this 89 | } 90 | return this.outerBorder(width, areaColors.focusColor, shape) 91 | } 92 | 93 | val LocalAreaColors = compositionLocalOf { 94 | LightTheme.NormalAreaColors 95 | } 96 | 97 | val LocalNormalAreaColors = compositionLocalOf { 98 | LightTheme.NormalAreaColors 99 | } 100 | 101 | val LocalInactiveAreaColors = compositionLocalOf { 102 | LightTheme.InactiveAreaColors 103 | } 104 | 105 | val LocalErrorAreaColors = compositionLocalOf { 106 | LightTheme.ErrorAreaColors 107 | } 108 | 109 | val LocalErrorInactiveAreaColors = compositionLocalOf { 110 | LightTheme.ErrorInactiveAreaColors 111 | } 112 | 113 | val LocalDisabledAreaColors = compositionLocalOf { 114 | LightTheme.DisabledAreaColors 115 | } 116 | 117 | val LocalHoverAreaColors = compositionLocalOf { 118 | LightTheme.HoverAreaColors 119 | } 120 | 121 | val LocalPressedAreaColors = compositionLocalOf { 122 | LightTheme.PressedAreaColors 123 | } 124 | 125 | val LocalFocusAreaColors = compositionLocalOf { 126 | LightTheme.FocusAreaColors 127 | } 128 | 129 | val LocalSelectionAreaColors = compositionLocalOf { 130 | LightTheme.SelectionAreaColors 131 | } 132 | 133 | val LocalSelectionInactiveAreaColors = compositionLocalOf { 134 | LightTheme.SelectionInactiveAreaColors 135 | } 136 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/AreaProvider.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.style 2 | 3 | interface AreaProvider { 4 | val normalAreaColors: AreaColors 5 | } 6 | 7 | interface ErrorAreProvider : AreaProvider { 8 | val errorAreaColors: AreaColors 9 | } 10 | 11 | interface ErrorFocusAreaProvider : ErrorAreProvider, FocusAreaProvider { 12 | val errorFocusAreaColors: AreaColors 13 | } 14 | 15 | interface SelectionAreaProvider : AreaProvider { 16 | val selectionAreaColors: AreaColors 17 | } 18 | 19 | interface FocusAreaProvider : AreaProvider { 20 | val focusAreaColors: AreaColors 21 | } 22 | 23 | interface DisabledAreaProvider : AreaProvider { 24 | val disabledAreaColors: AreaColors 25 | } 26 | 27 | interface HoverAreaProvider : AreaProvider { 28 | val hoverAreaColors: AreaColors 29 | } 30 | 31 | interface PressedAreaProvider : AreaProvider { 32 | val pressedAreaColors: AreaColors 33 | } 34 | 35 | interface InactiveAreaProvider : AreaProvider { 36 | val inactiveAreaColors: AreaColors 37 | } 38 | 39 | interface InactiveErrorAreaProvider : ErrorAreProvider, InactiveAreaProvider { 40 | val inactiveErrorAreaColors: AreaColors 41 | } 42 | 43 | interface InactiveSelectionAreaProvider : SelectionAreaProvider, InactiveAreaProvider { 44 | val inactiveSelectionAreaColors: AreaColors 45 | } 46 | 47 | interface InactiveFocusAreaProvider : FocusAreaProvider, InactiveAreaProvider { 48 | val inactiveFocusAreaColors: AreaColors 49 | } 50 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/TextStyle.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.style 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 5 | 6 | val LocalDefaultTextStyle = compositionLocalOf { 7 | LightTheme.DefaultTextStyle 8 | } 9 | 10 | val LocalDefaultBoldTextStyle = compositionLocalOf { 11 | LightTheme.DefaultBoldTextStyle 12 | } 13 | 14 | val LocalParagraphTextStyle = compositionLocalOf { 15 | LightTheme.ParagraphTextStyle 16 | } 17 | 18 | val LocalMediumTextStyle = compositionLocalOf { 19 | LightTheme.MediumTextStyle 20 | } 21 | 22 | val LocalMediumBoldTextStyle = compositionLocalOf { 23 | LightTheme.MediumBoldTextStyle 24 | } 25 | 26 | val LocalSmallTextStyle = compositionLocalOf { 27 | LightTheme.SmallTextStyle 28 | } 29 | 30 | val LocalH0TextStyle = compositionLocalOf { 31 | LightTheme.H0TextStyle 32 | } 33 | 34 | val LocalH1TextStyle = compositionLocalOf { 35 | LightTheme.H1TextStyle 36 | } 37 | 38 | val LocalH2TextStyle = compositionLocalOf { 39 | LightTheme.H2TextStyle 40 | } 41 | 42 | val LocalH2BoldTextStyle = compositionLocalOf { 43 | LightTheme.H2BoldTextStyle 44 | } 45 | 46 | val LocalH3TextStyle = compositionLocalOf { 47 | LightTheme.H3TextStyle 48 | } 49 | 50 | val LocalH3BoldTextStyle = compositionLocalOf { 51 | LightTheme.H3BoldTextStyle 52 | } -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/theme/Fonts.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.theme 2 | 3 | import androidx.compose.ui.text.font.FontFamily 4 | import androidx.compose.ui.text.font.FontWeight 5 | import androidx.compose.ui.text.platform.Font 6 | 7 | object Fonts { 8 | val InterRegular = Font(resource = "/fonts/Inter-Regular.ttf", weight = FontWeight.Normal) 9 | 10 | val InterBold = Font(resource = "/fonts/Inter-Bold.ttf", weight = FontWeight.Bold) 11 | 12 | val InterMedium = Font(resource = "/fonts/Inter-Medium.ttf", weight = FontWeight.Medium) 13 | 14 | val InterSemiBold = Font(resource = "/fonts/Inter-SemiBold.ttf", weight = FontWeight.SemiBold) 15 | 16 | val InterLight = Font(resource = "/fonts/Inter-Light.ttf", weight = FontWeight.Light) 17 | 18 | val InterThin = Font(resource = "/fonts/Inter-Thin.ttf", weight = FontWeight.Thin) 19 | 20 | val InterExtraLight = Font(resource = "/fonts/Inter-ExtraLight.ttf", weight = FontWeight.ExtraLight) 21 | 22 | val InterExtraBold = Font(resource = "/fonts/Inter-ExtraBold.ttf", weight = FontWeight.ExtraBold) 23 | 24 | val InterBlack = Font(resource = "/fonts/Inter-Black.ttf", weight = FontWeight.Black) 25 | 26 | val Inter = FontFamily( 27 | InterRegular, 28 | InterBold, 29 | InterMedium, 30 | InterSemiBold, 31 | InterLight, 32 | InterThin, 33 | InterExtraLight, 34 | InterExtraBold 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.ProvidedValue 6 | import androidx.compose.runtime.compositionLocalOf 7 | 8 | val LocalIsDarkTheme = compositionLocalOf { false } 9 | 10 | interface Theme { 11 | val isDark: Boolean 12 | 13 | @Composable 14 | fun provide(content: @Composable () -> Unit) { 15 | CompositionLocalProvider( 16 | *provideValues(), 17 | content = content, 18 | ) 19 | } 20 | 21 | fun provideValues(): Array> 22 | } 23 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/util/CustomWindowDecorationAccessing.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.util 2 | 3 | import java.awt.Shape 4 | import java.awt.Window 5 | import java.lang.reflect.Method 6 | 7 | internal object CustomWindowDecorationAccessing { 8 | init { 9 | UnsafeAccessing.assignAccessibility( 10 | UnsafeAccessing.desktopModule, 11 | listOf( 12 | "java.awt" 13 | ) 14 | ) 15 | } 16 | 17 | private val customWindowDecorationInstance: Any? = try { 18 | val customWindowDecoration = Class.forName("java.awt.Window\$CustomWindowDecoration") 19 | val constructor = customWindowDecoration.declaredConstructors.first() 20 | constructor.isAccessible = true 21 | constructor.newInstance() 22 | } catch (e: Exception) { 23 | null 24 | } 25 | 26 | private val setCustomDecorationEnabledMethod: Method? = 27 | getMethod("setCustomDecorationEnabled", Window::class.java, Boolean::class.java) 28 | 29 | private val setCustomDecorationTitleBarHeightMethod: Method? = 30 | getMethod("setCustomDecorationTitleBarHeight", Window::class.java, Int::class.java) 31 | 32 | private val setCustomDecorationHitTestSpotsMethod: Method? = 33 | getMethod("setCustomDecorationHitTestSpots", Window::class.java, MutableList::class.java) 34 | 35 | private fun getMethod(name: String, vararg params: Class<*>): Method? { 36 | return try { 37 | val clazz = Class.forName("java.awt.Window\$CustomWindowDecoration") 38 | val method = clazz.getDeclaredMethod( 39 | name, *params 40 | ) 41 | method.isAccessible = true 42 | method 43 | } catch (e: Exception) { 44 | null 45 | } 46 | } 47 | 48 | fun setCustomDecorationEnabled(window: Window, enabled: Boolean) { 49 | val instance = customWindowDecorationInstance ?: return 50 | val method = setCustomDecorationEnabledMethod ?: return 51 | method.invoke(instance, window, enabled) 52 | } 53 | 54 | fun setCustomDecorationTitleBarHeight(window: Window, height: Int) { 55 | val instance = customWindowDecorationInstance ?: return 56 | val method = setCustomDecorationTitleBarHeightMethod ?: return 57 | method.invoke(instance, window, height) 58 | } 59 | 60 | fun setCustomDecorationHitTestSpotsMethod(window: Window, spots: Map) { 61 | val instance = customWindowDecorationInstance ?: return 62 | val method = setCustomDecorationHitTestSpotsMethod ?: return 63 | method.invoke(instance, window, spots.entries.toMutableList()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/util/UnsafeAccessing.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.util 2 | 3 | import sun.misc.Unsafe 4 | import java.lang.reflect.AccessibleObject 5 | 6 | internal object UnsafeAccessing { 7 | private val unsafe: Any? by lazy { 8 | try { 9 | val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") 10 | theUnsafe.isAccessible = true 11 | theUnsafe.get(null) as Unsafe 12 | } catch (e: Throwable) { 13 | null 14 | } 15 | } 16 | 17 | val desktopModule by lazy { 18 | ModuleLayer.boot().findModule("java.desktop").get() 19 | } 20 | 21 | val ownerModule by lazy { 22 | this.javaClass.module 23 | } 24 | 25 | private val isAccessibleFieldOffset: Long? by lazy { 26 | try { 27 | (unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first")) 28 | } catch (e: Throwable) { 29 | null 30 | } 31 | } 32 | 33 | private val implAddOpens by lazy { 34 | try { 35 | Module::class.java.getDeclaredMethod( 36 | "implAddOpens", String::class.java, Module::class.java 37 | ).accessible() 38 | } catch (e: Throwable) { 39 | null 40 | } 41 | } 42 | 43 | fun assignAccessibility(obj: AccessibleObject) { 44 | try { 45 | val theUnsafe = unsafe as? Unsafe ?: return 46 | val offset = isAccessibleFieldOffset ?: return 47 | theUnsafe.putBooleanVolatile(obj, offset, true) 48 | } catch (e: Throwable) { 49 | // ignore 50 | } 51 | } 52 | 53 | fun assignAccessibility(module: Module, packages: List) { 54 | try { 55 | packages.forEach { 56 | implAddOpens?.invoke(module, it, ownerModule) 57 | } 58 | } catch (e: Throwable) { 59 | // ignore 60 | } 61 | } 62 | 63 | private class Parent { 64 | var first = false 65 | 66 | @Volatile 67 | var second: Any? = null 68 | } 69 | } 70 | 71 | internal fun T.accessible(): T { 72 | return apply { 73 | UnsafeAccessing.assignAccessibility(this) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.Linux.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.input.key.KeyEvent 14 | import androidx.compose.ui.platform.LocalWindowInfo 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.ui.window.FrameWindowScope 17 | import androidx.compose.ui.window.Window 18 | import androidx.compose.ui.window.WindowState 19 | import androidx.compose.ui.window.rememberWindowState 20 | import io.kanro.compose.jetbrains.expui.control.LocalContentActivated 21 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 22 | import io.kanro.compose.jetbrains.expui.style.areaBackground 23 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 24 | import io.kanro.compose.jetbrains.expui.theme.Theme 25 | 26 | @Composable 27 | internal fun JBWindowOnLinux( 28 | onCloseRequest: () -> Unit, 29 | state: WindowState = rememberWindowState(), 30 | visible: Boolean = true, 31 | title: String = "", 32 | theme: Theme = LightTheme, 33 | resizable: Boolean = true, 34 | enabled: Boolean = true, 35 | focusable: Boolean = true, 36 | alwaysOnTop: Boolean = false, 37 | onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, 38 | onKeyEvent: (KeyEvent) -> Boolean = { false }, 39 | mainToolBar: (@Composable MainToolBarScope.() -> Unit)?, 40 | content: @Composable FrameWindowScope.() -> Unit, 41 | ) { 42 | Window( 43 | onCloseRequest, 44 | state, 45 | visible, 46 | title, 47 | null, 48 | false, 49 | false, 50 | resizable, 51 | enabled, 52 | focusable, 53 | alwaysOnTop, 54 | onPreviewKeyEvent, 55 | onKeyEvent 56 | ) { 57 | CompositionLocalProvider( 58 | LocalWindow provides window, 59 | LocalContentActivated provides LocalWindowInfo.current.isWindowFocused, 60 | *theme.provideValues() 61 | ) { 62 | Column(Modifier.fillMaxSize()) { 63 | MainToolBarOnLinux(content = mainToolBar) 64 | Spacer(Modifier.fillMaxWidth().height(1.dp).background(LocalAreaColors.current.startBorderColor)) 65 | Box(Modifier.fillMaxSize().areaBackground()) { 66 | content() 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.MacOS.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.DisposableEffect 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.State 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.input.key.KeyEvent 20 | import androidx.compose.ui.platform.LocalWindowInfo 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.window.FrameWindowScope 23 | import androidx.compose.ui.window.Window 24 | import androidx.compose.ui.window.WindowPlacement 25 | import androidx.compose.ui.window.WindowState 26 | import androidx.compose.ui.window.rememberWindowState 27 | import io.kanro.compose.jetbrains.expui.control.LocalContentActivated 28 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 29 | import io.kanro.compose.jetbrains.expui.style.areaBackground 30 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 31 | import io.kanro.compose.jetbrains.expui.theme.Theme 32 | import java.awt.event.ComponentEvent 33 | import java.awt.event.ComponentListener 34 | 35 | @Composable 36 | internal fun JBWindowOnMacOS( 37 | onCloseRequest: () -> Unit, 38 | state: WindowState = rememberWindowState(), 39 | visible: Boolean = true, 40 | title: String = "", 41 | showTitle: Boolean = true, 42 | theme: Theme = LightTheme, 43 | resizable: Boolean = true, 44 | enabled: Boolean = true, 45 | focusable: Boolean = true, 46 | alwaysOnTop: Boolean = false, 47 | onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, 48 | onKeyEvent: (KeyEvent) -> Boolean = { false }, 49 | mainToolBar: (@Composable MainToolBarScope.() -> Unit)?, 50 | content: @Composable FrameWindowScope.() -> Unit, 51 | ) { 52 | Window( 53 | onCloseRequest, 54 | state, 55 | visible, 56 | title, 57 | null, 58 | false, 59 | false, 60 | resizable, 61 | enabled, 62 | focusable, 63 | alwaysOnTop, 64 | onPreviewKeyEvent, 65 | onKeyEvent 66 | ) { 67 | LaunchedEffect(Unit, theme) { 68 | val rootPane = window.rootPane 69 | rootPane.putClientProperty( 70 | "apple.awt.windowAppearance", 71 | if (theme.isDark) "NSAppearanceNameVibrantDark" else "NSAppearanceNameVibrantLight" 72 | ) 73 | } 74 | CompositionLocalProvider( 75 | LocalWindow provides window, 76 | LocalContentActivated provides LocalWindowInfo.current.isWindowFocused, 77 | *theme.provideValues() 78 | ) { 79 | Column(Modifier.fillMaxSize()) { 80 | val isFullscreen by rememberWindowIsFullscreen() 81 | MainToolBarOnMacOS(title, showTitle, isFullscreen, content = mainToolBar) 82 | Spacer(Modifier.fillMaxWidth().height(1.dp).background(LocalAreaColors.current.startBorderColor)) 83 | Box(Modifier.fillMaxSize().areaBackground()) { 84 | content() 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | @Composable 92 | fun FrameWindowScope.rememberWindowIsFullscreen(): State { 93 | val isFullscreen = remember { 94 | mutableStateOf(window.placement == WindowPlacement.Fullscreen) 95 | } 96 | DisposableEffect(window) { 97 | val listener = object : ComponentListener { 98 | override fun componentResized(e: ComponentEvent?) { 99 | isFullscreen.value = window.placement == WindowPlacement.Fullscreen 100 | } 101 | 102 | override fun componentMoved(e: ComponentEvent?) {} 103 | 104 | override fun componentShown(e: ComponentEvent?) {} 105 | 106 | override fun componentHidden(e: ComponentEvent?) {} 107 | } 108 | window.addComponentListener(listener) 109 | onDispose { 110 | window.removeComponentListener(listener) 111 | } 112 | } 113 | return isFullscreen 114 | } 115 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.Windows.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.painter.Painter 14 | import androidx.compose.ui.input.key.KeyEvent 15 | import androidx.compose.ui.platform.LocalWindowInfo 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.window.FrameWindowScope 18 | import androidx.compose.ui.window.Window 19 | import androidx.compose.ui.window.WindowState 20 | import androidx.compose.ui.window.rememberWindowState 21 | import io.kanro.compose.jetbrains.expui.control.LocalContentActivated 22 | import io.kanro.compose.jetbrains.expui.style.LocalAreaColors 23 | import io.kanro.compose.jetbrains.expui.style.areaBackground 24 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 25 | import io.kanro.compose.jetbrains.expui.theme.Theme 26 | 27 | @Composable 28 | internal fun JBWindowOnWindows( 29 | onCloseRequest: () -> Unit, 30 | state: WindowState = rememberWindowState(), 31 | visible: Boolean = true, 32 | title: String = "", 33 | showTitle: Boolean = true, 34 | theme: Theme = LightTheme, 35 | icon: Painter? = null, 36 | resizable: Boolean = true, 37 | enabled: Boolean = true, 38 | focusable: Boolean = true, 39 | alwaysOnTop: Boolean = false, 40 | onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, 41 | onKeyEvent: (KeyEvent) -> Boolean = { false }, 42 | mainToolBar: (@Composable MainToolBarScope.() -> Unit)?, 43 | content: @Composable FrameWindowScope.() -> Unit, 44 | ) { 45 | Window( 46 | onCloseRequest, 47 | state, 48 | visible, 49 | title, 50 | icon, 51 | false, 52 | false, 53 | resizable, 54 | enabled, 55 | focusable, 56 | alwaysOnTop, 57 | onPreviewKeyEvent, 58 | onKeyEvent 59 | ) { 60 | CompositionLocalProvider( 61 | LocalWindow provides window, 62 | LocalContentActivated provides LocalWindowInfo.current.isWindowFocused, 63 | *theme.provideValues() 64 | ) { 65 | Column(Modifier.fillMaxSize()) { 66 | MainToolBarOnWindows(icon, state, onCloseRequest, title, showTitle, resizable, content = mainToolBar) 67 | Spacer(Modifier.fillMaxWidth().height(1.dp).background(LocalAreaColors.current.startBorderColor)) 68 | Box(Modifier.fillMaxSize().areaBackground()) { 69 | content() 70 | } 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.compositionLocalOf 5 | import androidx.compose.ui.graphics.painter.Painter 6 | import androidx.compose.ui.input.key.KeyEvent 7 | import androidx.compose.ui.res.painterResource 8 | import androidx.compose.ui.window.FrameWindowScope 9 | import androidx.compose.ui.window.WindowState 10 | import androidx.compose.ui.window.rememberWindowState 11 | import io.kanro.compose.jetbrains.expui.DesktopPlatform 12 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 13 | import io.kanro.compose.jetbrains.expui.theme.Theme 14 | import javax.swing.JFrame 15 | 16 | val LocalWindow = compositionLocalOf { 17 | error("CompositionLocal LocalWindow not provided") 18 | } 19 | 20 | @Composable 21 | fun JBWindow( 22 | onCloseRequest: () -> Unit, 23 | state: WindowState = rememberWindowState(), 24 | visible: Boolean = true, 25 | title: String = "", 26 | showTitle: Boolean = true, 27 | theme: Theme = LightTheme, 28 | icon: Painter? = painterResource("icons/compose.svg"), 29 | resizable: Boolean = true, 30 | enabled: Boolean = true, 31 | focusable: Boolean = true, 32 | alwaysOnTop: Boolean = false, 33 | onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, 34 | onKeyEvent: (KeyEvent) -> Boolean = { false }, 35 | mainToolBar: (@Composable MainToolBarScope.() -> Unit)? = null, 36 | content: @Composable FrameWindowScope.() -> Unit, 37 | ) { 38 | when (DesktopPlatform.Current) { 39 | DesktopPlatform.Linux -> JBWindowOnLinux( 40 | onCloseRequest, 41 | state, 42 | visible, 43 | title, 44 | theme, 45 | resizable, 46 | enabled, 47 | focusable, 48 | alwaysOnTop, 49 | onPreviewKeyEvent, 50 | onKeyEvent, 51 | mainToolBar, 52 | content 53 | ) 54 | 55 | DesktopPlatform.Windows -> JBWindowOnWindows( 56 | onCloseRequest, 57 | state, 58 | visible, 59 | title, 60 | showTitle, 61 | theme, 62 | icon, 63 | resizable, 64 | enabled, 65 | focusable, 66 | alwaysOnTop, 67 | onPreviewKeyEvent, 68 | onKeyEvent, 69 | mainToolBar, 70 | content 71 | ) 72 | 73 | DesktopPlatform.MacOS -> JBWindowOnMacOS( 74 | onCloseRequest, 75 | state, 76 | visible, 77 | title, 78 | showTitle, 79 | theme, 80 | resizable, 81 | enabled, 82 | focusable, 83 | alwaysOnTop, 84 | onPreviewKeyEvent, 85 | onKeyEvent, 86 | mainToolBar, 87 | content 88 | ) 89 | 90 | DesktopPlatform.Unknown -> TODO() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.Basic.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.layout.Layout 8 | import androidx.compose.ui.unit.dp 9 | import androidx.compose.ui.window.FrameWindowScope 10 | import io.kanro.compose.jetbrains.expui.control.LocalContentActivated 11 | import io.kanro.compose.jetbrains.expui.style.areaBackground 12 | 13 | @Composable 14 | fun FrameWindowScope.BasicMainToolBar( 15 | colors: MainToolBarColors = LocalMainToolBarColors.current, 16 | content: (@Composable MainToolBarScope.() -> Unit)?, 17 | ) { 18 | colors.provideArea(LocalContentActivated.current) { 19 | Layout( 20 | content = { 21 | with(MainToolBarScopeInstance) { 22 | content?.invoke(this) 23 | } 24 | }, 25 | modifier = Modifier.fillMaxWidth().height(40.dp).areaBackground(), 26 | measurePolicy = rememberMainToolBarMeasurePolicy(window) 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.Linux.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.window.FrameWindowScope 5 | 6 | @Composable 7 | internal fun FrameWindowScope.MainToolBarOnLinux( 8 | colors: MainToolBarColors = LocalMainToolBarColors.current, 9 | content: (@Composable MainToolBarScope.() -> Unit)?, 10 | ) { 11 | BasicMainToolBar(colors, content) 12 | } -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.MacOS.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.width 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import androidx.compose.ui.window.FrameWindowScope 10 | 11 | @Composable 12 | internal fun FrameWindowScope.MainToolBarOnMacOS( 13 | title: String, 14 | showTitle: Boolean, 15 | isFullScreen: Boolean, 16 | colors: MainToolBarColors = LocalMainToolBarColors.current, 17 | content: (@Composable MainToolBarScope.() -> Unit)?, 18 | ) { 19 | BasicMainToolBar(colors) { 20 | if (isFullScreen) { 21 | Spacer(Modifier.width(80.dp).mainToolBarItem(Alignment.Start, true)) 22 | } 23 | if (showTitle) { 24 | MainToolBarTitle(title) 25 | } 26 | content?.invoke(this) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.Windows.kt: -------------------------------------------------------------------------------- 1 | package io.kanro.compose.jetbrains.expui.window 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.compositionLocalOf 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.focus.focusProperties 11 | import androidx.compose.ui.graphics.RectangleShape 12 | import androidx.compose.ui.graphics.painter.Painter 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.window.FrameWindowScope 15 | import androidx.compose.ui.window.WindowPlacement 16 | import androidx.compose.ui.window.WindowState 17 | import io.kanro.compose.jetbrains.expui.control.ActionButton 18 | import io.kanro.compose.jetbrains.expui.control.Icon 19 | import io.kanro.compose.jetbrains.expui.control.LocalActionButtonColors 20 | import io.kanro.compose.jetbrains.expui.control.LocalContentActivated 21 | import io.kanro.compose.jetbrains.expui.theme.LightTheme 22 | 23 | @Composable 24 | internal fun FrameWindowScope.MainToolBarOnWindows( 25 | icon: Painter?, 26 | windowState: WindowState, 27 | onCloseRequest: () -> Unit, 28 | title: String, 29 | showTitle: Boolean, 30 | resizeable: Boolean, 31 | colors: MainToolBarColors = LocalMainToolBarColors.current, 32 | content: (@Composable MainToolBarScope.() -> Unit)?, 33 | ) { 34 | BasicMainToolBar(colors) { 35 | if (icon != null) { 36 | Box( 37 | modifier = Modifier.size(40.dp).mainToolBarItem(Alignment.Start, true), 38 | contentAlignment = Alignment.Center 39 | ) { 40 | Icon(icon) 41 | } 42 | } 43 | WindowsSystemButtons(windowState, resizeable, onCloseRequest) 44 | if (showTitle) { 45 | MainToolBarTitle(title) 46 | } 47 | content?.invoke(this) 48 | } 49 | } 50 | 51 | val LocalWindowsCloseWindowButtonColors = compositionLocalOf { 52 | LightTheme.WindowsCloseWindowButtonColors 53 | } 54 | 55 | @Composable 56 | private fun MainToolBarScope.WindowsSystemButtons( 57 | windowState: WindowState, 58 | resizeable: Boolean, 59 | onCloseRequest: () -> Unit, 60 | ) { 61 | val active = LocalContentActivated.current 62 | CompositionLocalProvider( 63 | LocalActionButtonColors provides LocalWindowsCloseWindowButtonColors.current 64 | ) { 65 | ActionButton( 66 | { onCloseRequest() }, 67 | Modifier.focusProperties { canFocus = false }.size(40.dp).mainToolBarItem(Alignment.End), 68 | shape = RectangleShape 69 | ) { 70 | if (active) { 71 | Icon("icons/windows/closeActive.svg") 72 | } else { 73 | Icon("icons/windows/closeInactive.svg") 74 | } 75 | } 76 | } 77 | ActionButton( 78 | { 79 | windowState.placement = when (windowState.placement) { 80 | WindowPlacement.Floating -> WindowPlacement.Maximized 81 | WindowPlacement.Maximized -> WindowPlacement.Floating 82 | WindowPlacement.Fullscreen -> WindowPlacement.Fullscreen 83 | } 84 | }, 85 | Modifier.focusProperties { canFocus = false }.size(40.dp).mainToolBarItem(Alignment.End), 86 | enabled = resizeable, 87 | shape = RectangleShape 88 | ) { 89 | if (windowState.placement == WindowPlacement.Floating) { 90 | if (active) { 91 | Icon("icons/windows/maximize.svg") 92 | } else { 93 | Icon("icons/windows/maximizeInactive.svg") 94 | } 95 | } else { 96 | if (active) { 97 | Icon("icons/windows/restore.svg") 98 | } else { 99 | Icon("icons/windows/restoreInactive.svg") 100 | } 101 | } 102 | } 103 | ActionButton( 104 | { windowState.isMinimized = true }, 105 | Modifier.focusProperties { canFocus = false }.size(40.dp).mainToolBarItem(Alignment.End), 106 | shape = RectangleShape 107 | ) { 108 | if (active) { 109 | Icon("icons/windows/minimize.svg") 110 | } else { 111 | Icon("icons/windows/minimizeInactive.svg") 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-Black.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-Light.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/fonts/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/expui/src/main/resources/fonts/Inter-Thin.ttf -------------------------------------------------------------------------------- /expui/src/main/resources/icons/buttonDropTriangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/buttonDropTriangle_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/closeSmall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/closeSmallHovered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/closeSmallHovered_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/closeSmall_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/external_link_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/external_link_arrow_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/linkDropTriangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/linkDropTriangle_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/closeActive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/closeInactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/maximizeInactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/minimizeInactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /expui/src/main/resources/icons/windows/restoreInactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ButterCam/compose-jetbrains-theme/8f237fd0c144ee8cc425aff5430f48ae634e914e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | gradlePluginPortal() 5 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 6 | } 7 | } 8 | 9 | include("classic") 10 | include("expui") 11 | include("expui-gallery") 12 | 13 | project(":classic").name = "compose-jetbrains-theme" 14 | project(":expui").name = "compose-jetbrains-expui-theme" 15 | project(":expui-gallery").name = "compose-jetbrains-expui-gallery" 16 | 17 | --------------------------------------------------------------------------------