Repository: ButterCam/compose-jetbrains-theme Branch: main Commit: 8f237fd0c144 Files: 104 Total size: 428.5 KB Directory structure: gitextract_99aa8iq5/ ├── .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 ├── 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 ├── expui-gallery/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── Main.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) push: branches: [ main ] # Trigger the workflow on any pull request pull_request: jobs: # Run Gradle Wrapper Validation Action to verify the wrapper's checksum gradleValidation: name: Gradle Wrapper runs-on: ubuntu-latest steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.3.4 # Validate wrapper - name: Gradle Wrapper Validation uses: gradle/wrapper-validation-action@v1.0.4 build: name: Build runs-on: ubuntu-latest outputs: version: ${{ steps.properties.outputs.version }} changelog: ${{ steps.properties.outputs.changelog }} steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2 - name: Download JBR uses: carlosperate/download-file-action@v2 with: file-url: 'https://cache-redirector.jetbrains.com/intellij-jbr/jbr-17.0.5-linux-x64-b653.14.tar.gz' file-name: 'jbr-17.0.5.tar.gz' - name: Setup Java uses: actions/setup-java@v3 with: java-version: 17 distribution: jdkfile jdkFile: jbr-17.0.5.tar.gz cache: gradle # Set environment variables - name: Export Properties id: properties shell: bash run: | PROPERTIES="$(./gradlew properties -q)" VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" NAME="$(echo "$PROPERTIES" | grep "^name:" | cut -f2- -d ' ')" CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" CHANGELOG="${CHANGELOG//'%'/'%25'}" CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" echo "version=$VERSION" echo "name=$NAME" echo "changelog=$CHANGELOG" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT # Build artifact using buildPlugin Gradle task - name: Build run: ./gradlew build # Store built plugin as an artifact for downloading - name: Upload artifacts uses: actions/upload-artifact@v3 with: name: "${{ steps.properties.outputs.name }} - ${{ steps.properties.outputs.version }}" path: ./**/build/libs/* # Prepare a draft release for GitHub Releases page for the manual verification # If accepted and published, release workflow would be triggered releaseDraft: name: Release Draft if: github.event_name != 'pull_request' needs: build runs-on: ubuntu-latest steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.3.4 # Remove old release drafts by using the curl request for the available releases with draft flag - name: Remove Old Release Drafts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh api repos/{owner}/{repo}/releases \ --jq '.[] | select(.draft == true) | .id' \ | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} # Create new release draft - which is not publicly visible and requires manual acceptance - name: Create Release Draft env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create v${{ needs.build.outputs.version }} \ --draft \ --title "v${{ needs.build.outputs.version }}" \ --notes "${{ needs.build.outputs.changelog }}" ================================================ FILE: .github/workflows/release.yml ================================================ # GitHub Actions Workflow created for handling the release process based on the draft release prepared # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. name: Release on: release: types: [ prereleased, released ] jobs: # Prepare and publish the plugin to the Marketplace repository release: name: Publish Plugin runs-on: ubuntu-latest steps: # Check out current repository - name: Fetch Sources uses: actions/checkout@v2.3.4 with: ref: ${{ github.event.release.tag_name }} - name: Download JBR uses: carlosperate/download-file-action@v2 with: file-url: 'https://cache-redirector.jetbrains.com/intellij-jbr/jbr-17.0.5-linux-x64-b653.14.tar.gz' file-name: 'jbr-17.0.5.tar.gz' - name: Setup Java uses: actions/setup-java@v3 with: java-version: 17 distribution: jdkfile jdkFile: jbr-17.0.5.tar.gz cache: gradle # Update Unreleased section with the current release note - name: Patch Changelog run: | ./gradlew patchChangelog --release-note="`cat << EOM ${{ github.event.release.body }} EOM`" - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v3 with: gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - uses: ButterCam/setup-sisyphus-build@v1 with: dependency-repositories: local,central,portal,google,snapshot snapshot-url: https://s01.oss.sonatype.org/content/repositories/snapshots snapshot-username: ${{ secrets.OSSRH_USERNAME }} snapshot-password: ${{ secrets.OSSRH_PASSWORD }} release-url: https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 release-username: ${{ secrets.OSSRH_USERNAME }} release-password: ${{ secrets.OSSRH_PASSWORD }} gradle-portal-key: ${{ secrets.GRADLE_PUBLISH_KEY }} gradle-portal-secret: ${{ secrets.GRADLE_PUBLISH_SECRET }} gpg-key-name: ${{ secrets.GPG_KEY_NAME }} # Publish the plugin to the Marketplace - name: Publish env: PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} run: ./gradlew publish # Upload artifact as a release asset - name: Upload Release Asset env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload ${{ github.event.release.tag_name }} ./**/build/libs/* # Create pull request - name: Create Pull Request env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ github.event.release.tag_name }}" BRANCH="changelog-update-$VERSION" git config user.email "action@github.com" git config user.name "GitHub Action" git checkout -b $BRANCH git commit -am "Changelog update - $VERSION" git push --set-upstream origin $BRANCH gh pr create \ --title ":bookmark: Changelog update - \`$VERSION\`" \ --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ --base main \ --head $BRANCH ================================================ FILE: .gitignore ================================================ .gradle /build/ !foundation/gradle/wrapper/gradle-wrapper.jar ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### nbproject/private/ build/ nbbuild/ dist/ nbdist/ .nb-gradle/ logs/ out/ data/elasticsearch .DS_Store ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [Unreleased] ## [2.2.0] - Upgrade to compose 1.5.2 ## [2.1.0] - Provide BasicMainToolBar #18 - Disable minimize and maximize button when resizeable is false #19 - Fix the custom title bar when MainToolBar Content is empty #20 - Optimize the implementation of themes. Switching themes will not cause full recompose. - Add text styles for theme ## [2.0.0] - Exp UI theme support ## [1.1.0] - Target to jvm 1.11 #11 - Replacing SVG Resources with Compose generated Image Vectors #13 - IntelliJ Dracula colors (Dark Theme) #12 Thanks @DevSrSouza ## [1.0.1] - Upgrade to compose 1.0.1 ## [1.0.0] - JetBrains UI Kit for Compose desktop ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 ButterCam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # We are planning to migrate to [JetBrains/jewel](https://github.com/JetBrains/jewel)! 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. Track progress in this issue: JetBrains/jewel#28 # JetBrains UI Kit for Compose Desktop New JetBrains style controls and UI kits for [Compose Desktop](https://www.jetbrains.com/lp/compose/). Classic JetBrains UI kit have been moved to [here](classic). ![screenshot](docs/screenshot-expui.png) ## Quick Start ## Requirements ### JetBrains Runtime To get the best presentation across platforms, this library requires the application to run on [JetBrains Runtime](https://github.com/JetBrains/JetBrainsRuntime) (JBR). [JetBrains Runtime](https://github.com/JetBrains/JetBrainsRuntime) is a modified version of the JetBrains JDK distribution, which is widely used across the JetBrains' IDEs such as IntelliJ, GoLand and others. In this library we use the additional API provided by JetBrains Runtime to customise the title bar in Windows and macOS. If the `JBWindow` component is not used, it can also be used without the JetBrains Runtime. When using `JBWindow` in a non-JetBrains Runtime environment, a native title bar may additionally be displayed. JBR-17 (corresponding to JDK 17) can be downloaded from [the SDK page](https://www.jetbrains.com/help/idea/sdk.html) of IntelliJ. Make sure that the [project SDK](https://www.jetbrains.com/help/idea/project-settings-and-structure.html#project-sdk) and [Gradle JVM](https://www.jetbrains.com/help/idea/gradle-jvm-selection.html) are both JBR-17. #### Project SDK settings ![](docs/project-sdk.png) #### Gradle JVM settings ![](docs/gradle-jvm.png) ### `jdk.unsupported` module Most of the JetBrains Runtime APIs are private to JetBrains, and the class `sun.misc.Unsafe` is used to get access to these APIs. You need to add the `jdk.unsupported` module to the compose dependency as in the following code to make it work. You can also skip this step if you don't need to use the `JBWindow` component. ```kotlin compose.desktop { application { nativeDistributions { modules("jdk.unsupported") } } } ``` ## 1. Add dependency ```kotlin dependencies { implementation(compose.desktop.currentOs) { exclude("org.jetbrains.compose.material") } implementation("com.bybutter.compose:compose-jetbrains-expui-theme:2.0.0") } ``` ## 2. Use JBWindow DSL The optimal display of JBWindow requires JetBrains Runtime, refer to the previous [requirements](#Requirements). ```kotlin fun main() = application { JBWindow( title = "JetBrains ExpUI Gallery", 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). theme = LightTheme, // Change the theme here, LightTheme and DarkTheme are provided. state = rememberWindowState(size = DpSize(900.dp, 700.dp)), onCloseRequest = { exitApplication() }, mainToolBar = { // Render your own component in the MainToolBar (TitleBar). Row( Modifier.mainToolBarItem( Alignment.End, true ) ) { // Use the mainToolBarItem modifier to change alignment of components and enable/disable window drag area on this component. } }) { // Window content } } ``` ## 3. Use LightTheme and DarkTheme DSL Since `JBWindow` requires JetBrains Runtime, if you don't want to use the `JBWindow` component, you can also use a normal `Window` with `LightTheme`/`DarkTheme` DSL. ```kotlin fun main() = application { Window({}) { LightTheme { // Your components here } } } ``` # Screenshot ## MacOS 截屏2022-11-21 17 11 56 截屏2022-11-21 17 12 18 ## Windows ![image](https://user-images.githubusercontent.com/9367842/203374673-be0e25ab-de0c-4682-a511-bbadf0949446.png) ![image](https://user-images.githubusercontent.com/9367842/203374902-0b384bdd-fff4-4cbc-a8eb-93d6b58229a2.png) ## Linux(Ubuntu with Gnome) ![image](https://user-images.githubusercontent.com/9367842/203574847-6b6a3d4a-0da2-41f8-ac3f-9a8402f26b16.png) ![image](https://user-images.githubusercontent.com/9367842/203574918-953092bc-b00f-455b-a43a-237c63093095.png) ================================================ FILE: build.gradle.kts ================================================ plugins { id("com.bybutter.sisyphus.project") version "2.1.0" apply false id("com.netflix.nebula.contacts") version "7.0.1" id("com.netflix.nebula.info") version "12.1.6" apply false id("com.netflix.nebula.maven-publish") version "20.3.0" apply false id("com.netflix.nebula.source-jar") version "20.3.0" apply false id("org.jetbrains.changelog") version "1.3.0" id("org.jetbrains.compose") version "1.5.2" apply false kotlin("jvm") version "1.9.10" apply false } allprojects { apply(plugin = "com.netflix.nebula.contacts") apply(plugin = "com.netflix.nebula.info") group = "com.bybutter.compose" version = "2.2.0" contacts { addPerson( "higan@live.cn", delegateClosureOf { moniker = "higan" github = "devkanro" roles.add("owner") }, ) } } subprojects { apply(plugin = "org.jetbrains.compose") tasks.withType() { kotlinOptions.jvmTarget = "17" } repositories { mavenLocal() mavenCentral() google() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } changelog { version.set(project.version.toString()) groups.set(emptyList()) } ================================================ FILE: classic/README.md ================================================ # JetBrains UI Kit for Compose Desktop JetBrains style controls and UI for [Compose Desktop](https://www.jetbrains.com/lp/compose/). ![screenshot](../docs/screenshot.png) ## Quick Start ## 1. Add dependency ```kotlin dependencies { implementation(compose.desktop.currentOs) { exclude("org.jetbrains.compose.material") } implementation("com.bybutter.compose:compose-jetbrains-theme:2.0.0") } ``` ## 2. JBTheme DSL ```kotlin fun main() = application { Window( onCloseRequest = ::exitApplication, title = "Compose for Desktop", state = rememberWindowState(width = 300.dp, height = 300.dp) ) { val count = remember { mutableStateOf(0) } JBTheme { JPanel(Modifier.fillMaxSize().jBorder(top = 1.dp, color = JBTheme.panelColors.border)) { Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Button(modifier = Modifier.align(Alignment.CenterHorizontally), onClick = { count.value++ }) { Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!") } Button(modifier = Modifier.align(Alignment.CenterHorizontally), onClick = { count.value = 0 }) { Text("Reset") } } } } } } ``` ================================================ FILE: classic/build.gradle.kts ================================================ plugins { kotlin("jvm") `java-library` id("com.netflix.nebula.maven-publish") id("com.netflix.nebula.source-jar") id("com.bybutter.sisyphus.project") id("org.jetbrains.compose") } description = "JetBrains UI Kit for Compose Desktop" dependencies { implementation(kotlin("stdlib")) implementation(compose.desktop.common) { exclude("org.jetbrains.compose.material") } } tasks.withType() { kotlinOptions.jvmTarget = "17" kotlinOptions.freeCompilerArgs += listOf( // "-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true" ) } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/JBTheme.kt ================================================ package io.kanro.compose.jetbrains import androidx.compose.foundation.LocalScrollbarStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color import io.kanro.compose.jetbrains.color.ButtonColors import io.kanro.compose.jetbrains.color.CheckBoxColors import io.kanro.compose.jetbrains.color.FieldColors import io.kanro.compose.jetbrains.color.FocusColors import io.kanro.compose.jetbrains.color.IconColors import io.kanro.compose.jetbrains.color.LocalButtonColors import io.kanro.compose.jetbrains.color.LocalCheckBoxColors import io.kanro.compose.jetbrains.color.LocalFieldColors import io.kanro.compose.jetbrains.color.LocalFocusColors import io.kanro.compose.jetbrains.color.LocalIconColors import io.kanro.compose.jetbrains.color.LocalPanelColors import io.kanro.compose.jetbrains.color.LocalProgressColors import io.kanro.compose.jetbrains.color.LocalScrollColors import io.kanro.compose.jetbrains.color.LocalSelectionColors import io.kanro.compose.jetbrains.color.LocalTabColors import io.kanro.compose.jetbrains.color.LocalTextColors import io.kanro.compose.jetbrains.color.LocalToolBarColors import io.kanro.compose.jetbrains.color.PanelColors import io.kanro.compose.jetbrains.color.ProgressColors import io.kanro.compose.jetbrains.color.ScrollColors import io.kanro.compose.jetbrains.color.SelectionColors import io.kanro.compose.jetbrains.color.TabColors import io.kanro.compose.jetbrains.color.TextColors import io.kanro.compose.jetbrains.color.ToolBarColors import io.kanro.compose.jetbrains.color.darkButtonColors import io.kanro.compose.jetbrains.color.darkCheckBoxColors import io.kanro.compose.jetbrains.color.darkFieldColors import io.kanro.compose.jetbrains.color.darkFocusColors import io.kanro.compose.jetbrains.color.darkIconColors import io.kanro.compose.jetbrains.color.darkPanelColors import io.kanro.compose.jetbrains.color.darkProgressColors import io.kanro.compose.jetbrains.color.darkScrollColors import io.kanro.compose.jetbrains.color.darkSelectionColors import io.kanro.compose.jetbrains.color.darkTabColors import io.kanro.compose.jetbrains.color.darkTextColors import io.kanro.compose.jetbrains.color.darkToolBarColors import io.kanro.compose.jetbrains.color.lightButtonColors import io.kanro.compose.jetbrains.color.lightCheckBoxColors import io.kanro.compose.jetbrains.color.lightFieldColors import io.kanro.compose.jetbrains.color.lightFocusColors import io.kanro.compose.jetbrains.color.lightIconColors import io.kanro.compose.jetbrains.color.lightPanelColors import io.kanro.compose.jetbrains.color.lightProgressColors import io.kanro.compose.jetbrains.color.lightScrollColors import io.kanro.compose.jetbrains.color.lightSelectionColors import io.kanro.compose.jetbrains.color.lightTabColors import io.kanro.compose.jetbrains.color.lightTextColors import io.kanro.compose.jetbrains.color.lightToolBarColors import io.kanro.compose.jetbrains.control.LocalContentColor import io.kanro.compose.jetbrains.control.ProvideTextStyle import io.kanro.compose.jetbrains.control.darkSelectionScope import io.kanro.compose.jetbrains.control.lightSelectionScope @Composable fun JBTheme(style: JBThemeStyle, content: @Composable () -> Unit) { JBTheme( style = style, buttonColors = if (style == JBThemeStyle.LIGHT) lightButtonColors() else darkButtonColors(), fieldColors = if (style == JBThemeStyle.LIGHT) lightFieldColors() else darkFieldColors(), focusColors = if (style == JBThemeStyle.LIGHT) lightFocusColors() else darkFocusColors(), panelColors = if (style == JBThemeStyle.LIGHT) lightPanelColors() else darkPanelColors(), textColors = if (style == JBThemeStyle.LIGHT) lightTextColors() else darkTextColors(), toolBarColors = if (style == JBThemeStyle.LIGHT) lightToolBarColors() else darkToolBarColors(), progressColors = if (style == JBThemeStyle.LIGHT) lightProgressColors() else darkProgressColors(), scrollColors = if (style == JBThemeStyle.LIGHT) lightScrollColors() else darkScrollColors(), tabColors = if (style == JBThemeStyle.LIGHT) lightTabColors() else darkTabColors(), selectionColors = if (style == JBThemeStyle.LIGHT) lightSelectionColors() else darkSelectionColors(), checkBoxColors = if (style == JBThemeStyle.LIGHT) lightCheckBoxColors() else darkCheckBoxColors(), iconColors = if (style == JBThemeStyle.LIGHT) lightIconColors() else darkIconColors(), typography = JBTypography(), iconTheme = if (style == JBThemeStyle.LIGHT) JBThemeStyle.LIGHT else JBThemeStyle.DARK, selectionScope = if (style == JBThemeStyle.LIGHT) lightSelectionScope else darkSelectionScope, content = content ) } @Composable fun JBDraculaTheme(content: @Composable () -> Unit) { JBTheme( style = JBThemeStyle.DARK, buttonColors = darkButtonColors(), fieldColors = darkFieldColors(), focusColors = darkFocusColors(), panelColors = darkPanelColors(), textColors = darkTextColors(), toolBarColors = darkToolBarColors(), progressColors = darkProgressColors(), scrollColors = darkScrollColors(), tabColors = darkTabColors(), selectionColors = darkSelectionColors(), checkBoxColors = darkCheckBoxColors(), iconColors = darkIconColors(), typography = JBTypography(), iconTheme = JBThemeStyle.DARK, selectionScope = darkSelectionScope, content = content ) } @Composable fun JBLightTheme(content: @Composable () -> Unit) { JBTheme(content = content) } @Composable fun JBTheme( style: JBThemeStyle = JBThemeStyle.LIGHT, buttonColors: ButtonColors = lightButtonColors(), fieldColors: FieldColors = lightFieldColors(), focusColors: FocusColors = lightFocusColors(), panelColors: PanelColors = lightPanelColors(), textColors: TextColors = lightTextColors(), toolBarColors: ToolBarColors = lightToolBarColors(), progressColors: ProgressColors = lightProgressColors(), scrollColors: ScrollColors = lightScrollColors(), tabColors: TabColors = lightTabColors(), selectionColors: SelectionColors = lightSelectionColors(), checkBoxColors: CheckBoxColors = lightCheckBoxColors(), iconColors: IconColors = lightIconColors(), typography: JBTypography = JBTypography(), iconTheme: JBThemeStyle = JBThemeStyle.LIGHT, selectionScope: @Composable () -> Array> = lightSelectionScope, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalThemeStyle provides style, LocalButtonColors provides buttonColors, LocalFieldColors provides fieldColors, LocalFocusColors provides focusColors, LocalPanelColors provides panelColors, LocalTextColors provides textColors, LocalToolBarColors provides toolBarColors, LocalProgressColors provides progressColors, LocalScrollColors provides scrollColors, LocalTabColors provides tabColors, LocalSelectionColors provides selectionColors, LocalCheckBoxColors provides checkBoxColors, LocalIconColors provides iconColors, LocalTypography provides typography, LocalIconTheme provides iconTheme, LocalContentColor provides Color.Unspecified, LocalSelectionScope provides selectionScope, LocalScrollbarStyle provides scrollColors.style() ) { ProvideTextStyle(value = typography.default, content = content) } } object JBTheme { val style: JBThemeStyle @Composable @ReadOnlyComposable get() = LocalThemeStyle.current val buttonColors: ButtonColors @Composable @ReadOnlyComposable get() = LocalButtonColors.current val fieldColors: FieldColors @Composable @ReadOnlyComposable get() = LocalFieldColors.current val focusColors: FocusColors @Composable @ReadOnlyComposable get() = LocalFocusColors.current val panelColors: PanelColors @Composable @ReadOnlyComposable get() = LocalPanelColors.current val textColors: TextColors @Composable @ReadOnlyComposable get() = LocalTextColors.current val toolBarColors: ToolBarColors @Composable @ReadOnlyComposable get() = LocalToolBarColors.current val progressColors: ProgressColors @Composable @ReadOnlyComposable get() = LocalProgressColors.current val scrollColors: ScrollColors @Composable @ReadOnlyComposable get() = LocalScrollColors.current val tabColors: TabColors @Composable @ReadOnlyComposable get() = LocalTabColors.current val selectionColors: SelectionColors @Composable @ReadOnlyComposable get() = LocalSelectionColors.current val checkBoxColors: CheckBoxColors @Composable @ReadOnlyComposable get() = LocalCheckBoxColors.current val iconColors: IconColors @Composable @ReadOnlyComposable get() = LocalIconColors.current val typography: JBTypography @Composable @ReadOnlyComposable get() = LocalTypography.current val iconTheme: JBThemeStyle @Composable @ReadOnlyComposable get() = LocalIconTheme.current } enum class JBThemeStyle { LIGHT, DARK } val LocalThemeStyle = compositionLocalOf { JBThemeStyle.LIGHT } val LocalIconTheme = compositionLocalOf { JBThemeStyle.LIGHT } val LocalSelectionScope = compositionLocalOf<@Composable () -> Array>> { lightSelectionScope } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/JBTypography.kt ================================================ package io.kanro.compose.jetbrains import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp class JBTypography( val h0: TextStyle, val h1: TextStyle, val h2: TextStyle, val h2Bold: TextStyle, val h3: TextStyle, val h3Bold: TextStyle, val default: TextStyle, val defaultBold: TextStyle, val defaultUnderlined: TextStyle, val paragraph: TextStyle, val medium: TextStyle, val mediumBold: TextStyle, val small: TextStyle, val smallUnderlined: TextStyle, ) { constructor( defaultFontFamily: FontFamily = FontFamily.Default, h0: TextStyle = TextStyle( fontWeight = FontWeight.Medium, fontSize = 25.sp ), h1: TextStyle = TextStyle( fontWeight = FontWeight.Medium, fontSize = 22.sp ), h2: TextStyle = TextStyle( fontWeight = FontWeight.Normal, fontSize = 18.sp, ), h2Bold: TextStyle = h2.copy(fontWeight = FontWeight.Medium), h3: TextStyle = TextStyle( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 20.sp ), h3Bold: TextStyle = h3.copy(fontWeight = FontWeight.Medium), default: TextStyle = TextStyle( fontWeight = FontWeight.Normal, fontSize = 12.5.sp, lineHeight = 15.sp ), defaultBold: TextStyle = default.copy(fontWeight = FontWeight.Medium), defaultUnderlined: TextStyle = default.copy(textDecoration = TextDecoration.Underline), paragraph: TextStyle = TextStyle( fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 19.sp ), medium: TextStyle = TextStyle( fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 15.sp ), mediumBold: TextStyle = medium.copy(fontWeight = FontWeight.Medium), small: TextStyle = TextStyle( fontWeight = FontWeight.Normal, fontSize = 11.sp, lineHeight = 14.sp ), smallUnderlined: TextStyle = small.copy(textDecoration = TextDecoration.Underline), ) : this( h0.withDefaultFontFamily(defaultFontFamily), h1.withDefaultFontFamily(defaultFontFamily), h2.withDefaultFontFamily(defaultFontFamily), h2Bold.withDefaultFontFamily(defaultFontFamily), h3.withDefaultFontFamily(defaultFontFamily), h3Bold.withDefaultFontFamily(defaultFontFamily), default.withDefaultFontFamily(defaultFontFamily), defaultBold.withDefaultFontFamily(defaultFontFamily), defaultUnderlined.withDefaultFontFamily(defaultFontFamily), paragraph.withDefaultFontFamily(defaultFontFamily), medium.withDefaultFontFamily(defaultFontFamily), mediumBold.withDefaultFontFamily(defaultFontFamily), small.withDefaultFontFamily(defaultFontFamily), smallUnderlined.withDefaultFontFamily(defaultFontFamily) ) companion object { private fun TextStyle.withDefaultFontFamily(default: FontFamily): TextStyle { return if (fontFamily != null) this else copy(fontFamily = default) } } } val LocalTypography = compositionLocalOf { JBTypography() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ButtonColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import io.kanro.compose.jetbrains.JBTheme class ButtonColors( bg: Color, border: Color, borderRegularFocused: Color, defaultStart: Color, defaultEnd: Color, borderDefaultStart: Color, borderDefaultEnd: Color, borderDefaultFocused: Color, bgDisabled: Color, borderDisabled: Color, ) { var bg by mutableStateOf(bg) var border by mutableStateOf(border) var borderRegularFocused by mutableStateOf(borderRegularFocused) var defaultStart by mutableStateOf(defaultStart) var defaultEnd by mutableStateOf(defaultEnd) var borderDefaultStart by mutableStateOf(borderDefaultStart) var borderDefaultEnd by mutableStateOf(borderDefaultEnd) var borderDefaultFocused by mutableStateOf(borderDefaultFocused) var bgDisabled by mutableStateOf(bgDisabled) var borderDisabled by mutableStateOf(borderDisabled) fun copy( bg: Color = this.bg, border: Color = this.border, borderRegularFocused: Color = this.borderRegularFocused, defaultStart: Color = this.defaultStart, defaultEnd: Color = this.defaultEnd, borderDefaultStart: Color = this.borderDefaultStart, borderDefaultEnd: Color = this.borderDefaultEnd, borderDefaultFocused: Color = this.borderDefaultFocused, disabled: Color = this.bgDisabled, borderDisabled: Color = this.borderDisabled, ): ButtonColors { return ButtonColors( bg, border, borderRegularFocused, defaultStart, defaultEnd, borderDefaultStart, borderDefaultEnd, borderDefaultFocused, disabled, borderDisabled ) } @Composable private fun bgStartColor(enable: Boolean, default: Boolean): State { val targetValue = when { !enable -> bgDisabled default -> defaultStart else -> bg } return rememberUpdatedState(targetValue) } @Composable private fun bgEndColor(enable: Boolean, default: Boolean): State { val targetValue = when { !enable -> bgDisabled default -> defaultStart else -> bg } return rememberUpdatedState(targetValue) } @Composable fun bgBrush(enable: Boolean, default: Boolean): State { val start by bgStartColor(enable, default) val end by bgEndColor(enable, default) val targetValue = if (start == end) { SolidColor(start) } else { Brush.linearGradient(listOf(start, end)) } return rememberUpdatedState(targetValue) } @Composable private fun borderStartColor(enable: Boolean, default: Boolean, focused: Boolean): State { val targetValue = when { !enable -> borderDisabled default && focused -> borderDefaultFocused default -> borderDefaultStart focused -> borderRegularFocused else -> border } return rememberUpdatedState(targetValue) } @Composable private fun borderEndColor(enable: Boolean, default: Boolean, focused: Boolean): State { val targetValue = when { !enable -> borderDisabled default && focused -> borderDefaultFocused default -> borderDefaultEnd focused -> borderRegularFocused else -> border } return rememberUpdatedState(targetValue) } @Composable fun borderBrush(enable: Boolean, default: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() val start by borderStartColor(enable, default, focused) val end by borderEndColor(enable, default, focused) val targetValue = if (start == end) { SolidColor(start) } else { Brush.linearGradient(listOf(start, end)) } return rememberUpdatedState(targetValue) } @Composable fun textColor(enable: Boolean, default: Boolean): State { val start by bgStartColor(enable, default) val targetValue = if ((start.green + start.blue + start.red) / 3.0 > 0.6) { JBTheme.textColors.default } else { JBTheme.textColors.white } return rememberUpdatedState(targetValue) } @Composable fun focusingColor(enable: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() val targetValue = when { !enable -> Color.Transparent focused -> JBTheme.focusColors.default else -> Color.Transparent } return rememberUpdatedState(targetValue) } } fun lightButtonColors(): ButtonColors { return ButtonColors( Color.White, Color(0xFFC4C4C4), Color(0xFF87AFDA), Color(0xFF528CC7), Color(0xFF4989CC), Color(0xFF487EB8), Color(0xFF346DAD), Color(0xFFA8CEF6), Color(0xFFF2F2F2), Color(0xFFCFCFCF), ) } fun darkButtonColors(): ButtonColors { return ButtonColors( Color(0xFF4C5052), Color(0xFF5E6060), Color(0xFF456A90), Color(0xFF365880), Color(0xFF365880), Color(0xFF4C708C), Color(0xFF4C708C), Color(0xFF537699), Color(0xFF3C3F41), Color(0xFF646464), ) } val LocalButtonColors = compositionLocalOf { lightButtonColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/CheckBoxColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class CheckBoxColors( bg: Color, bgSelected: Color, bgDisabled: Color, border: Color, borderSelected: Color, borderFocused: Color, borderDisabled: Color, ) { var bg by mutableStateOf(bg) var bgSelected by mutableStateOf(bgSelected) var bgDisabled by mutableStateOf(bgDisabled) var border by mutableStateOf(border) var borderSelected by mutableStateOf(borderSelected) var borderFocused by mutableStateOf(borderFocused) var borderDisabled by mutableStateOf(borderDisabled) fun copy( bg: Color = this.bg, bgSelected: Color = this.bgSelected, bgDisabled: Color = this.bgDisabled, border: Color = this.border, borderSelected: Color = this.borderSelected, borderFocused: Color = this.borderFocused, borderDisabled: Color = this.borderDisabled, ): CheckBoxColors { return CheckBoxColors( bg, bgSelected, bgDisabled, border, borderSelected, borderFocused, borderDisabled ) } } fun lightCheckBoxColors(): CheckBoxColors { return CheckBoxColors( Color.White, Color(0xFF4F9EE3), Color(0xFFF2F2F2), Color(0xFFB0B0B0), Color(0xFF4B97D9), Color(0xFF7B9FC7), Color(0xFFBDBDBD), ) } fun darkCheckBoxColors(): CheckBoxColors { return CheckBoxColors( Color(0xFF43494A), Color(0xFF43494A), Color(0xFF3C3F41), Color(0xFF6B6B6B), Color(0xFF4C708C), Color(0xFF43698E), Color(0xFF545556), ) } val LocalCheckBoxColors = compositionLocalOf { lightCheckBoxColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/FieldColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import io.kanro.compose.jetbrains.JBTheme class FieldColors( bg: Color, border: Color, borderFocused: Color, comboboxButton: Color, bgDisabled: Color, borderDisabled: Color, borderError: Color, ) { var bg by mutableStateOf(bg) var border by mutableStateOf(border) var borderFocused by mutableStateOf(borderFocused) var comboboxButton by mutableStateOf(comboboxButton) var bgDisabled by mutableStateOf(bgDisabled) var borderDisabled by mutableStateOf(borderDisabled) var borderError by mutableStateOf(borderError) fun copy( bg: Color = this.bg, border: Color = this.border, borderFocused: Color = this.borderFocused, comboboxButton: Color = this.comboboxButton, bgDisabled: Color = this.bgDisabled, borderDisabled: Color = this.borderDisabled, borderError: Color = this.borderError, ): FieldColors { return FieldColors(bg, border, borderFocused, comboboxButton, bgDisabled, borderDisabled, borderError) } @Composable fun bgColor(enable: Boolean): State { return rememberUpdatedState(if (enable) bg else bgDisabled) } @Composable fun borderColor(enable: Boolean, error: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() val targetValue = when { !enable -> borderDisabled error -> borderError focused -> borderFocused else -> border } return if (enable) { animateColorAsState(targetValue, tween(durationMillis = AnimationDuration)) } else { rememberUpdatedState(targetValue) } } @Composable fun focusingColor(enable: Boolean, error: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() val targetValue = when { !enable -> Color.Transparent error -> JBTheme.focusColors.error focused -> JBTheme.focusColors.default else -> Color.Transparent } return rememberUpdatedState(targetValue) } companion object { internal const val AnimationDuration = 150 } } fun lightFieldColors(): FieldColors { return FieldColors( Color.White, Color(0xFFC4C4C4), Color(0xFF87AFDA), Color(0xFFFAFAFA), Color(0xFFF2F2F2), Color(0xFFCFCFCF), Color(0xFFCE3845), ) } fun darkFieldColors(): FieldColors { return FieldColors( Color(0xFF4C5052), Color(0xFF5E6060), Color(0xFF456A90), Color(0xFF3C3F41), Color(0xFF3C3F41), Color(0xFF646464), Color(0xFF73454B), ) } val LocalFieldColors = compositionLocalOf { lightFieldColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/FocusColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class FocusColors( default: Color, error: Color, warning: Color, warningInactive: Color, ) { var default by mutableStateOf(default) var error by mutableStateOf(error) var warning by mutableStateOf(warning) var warningInactive by mutableStateOf(warningInactive) fun copy( default: Color = this.default, error: Color = this.error, warning: Color = this.warning, warningInactive: Color = this.warningInactive, ): FocusColors { return FocusColors(default, error, warning, warningInactive) } } fun lightFocusColors(): FocusColors { return FocusColors( Color(0xFF97C3F3), Color(0xFFE53E4D), Color(0xFFE1A336), Color(0xFFEAD2A1), ) } fun darkFocusColors(): FocusColors { return FocusColors( Color(0xFF3D6185), Color(0xFF8B3C3C), Color(0xFFAC7920), Color(0xFF6E5324), ) } val LocalFocusColors = compositionLocalOf { lightFocusColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/IconColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class IconColors( selected: Color, disabled: Color, ) { var selected by mutableStateOf(selected) var disabled by mutableStateOf(disabled) fun copy( selected: Color = this.selected, disabled: Color = this.disabled, ): IconColors { return IconColors( selected, disabled, ) } } fun lightIconColors(): IconColors { return IconColors( Color.White, Color(0xFFABABAB), ) } fun darkIconColors(): IconColors { return IconColors( Color(0xFFFEFEFE), Color(0xFFABABAB), ) } val LocalIconColors = compositionLocalOf { lightIconColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/PanelColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class PanelColors( border: Color, bgContent: Color, bgDialog: Color, ) { var border by mutableStateOf(border) var bgContent by mutableStateOf(bgContent) var bgDialog by mutableStateOf(bgDialog) fun copy( border: Color = this.border, bgContent: Color = this.bgContent, bgDialog: Color = this.bgDialog, ): PanelColors { return PanelColors(border, bgContent, bgDialog) } } fun lightPanelColors(): PanelColors { return PanelColors( Color(0xFFD1D1D1), Color.White, Color(0xFFF2F2F2), ) } fun darkPanelColors(): PanelColors { return PanelColors( Color(0xFF323232), Color(0xFF3C3F41), Color(0xFF3C3F41), ) } val LocalPanelColors = compositionLocalOf { lightPanelColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ProgressColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class ProgressColors( progress: Color, bg: Color, ) { var progress by mutableStateOf(progress) var bg by mutableStateOf(bg) fun copy( progress: Color = this.progress, bg: Color = this.bg, ): ProgressColors { return ProgressColors(progress, bg) } } fun lightProgressColors(): ProgressColors { return ProgressColors( Color(0xFF1E82E6), Color(0xFFD5D5D5), ) } fun darkProgressColors(): ProgressColors { return ProgressColors( Color(0xFFA0A0A0), Color(0xFF555555), ) } val LocalProgressColors = compositionLocalOf { lightProgressColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ScrollColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.foundation.ScrollbarStyle import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp class ScrollColors( bg: Color, ) { var bg by mutableStateOf(bg) fun copy( bg: Color = this.bg, ): ScrollColors { return ScrollColors(bg) } fun style(): ScrollbarStyle { return ScrollbarStyle( minimalHeight = 30.dp, thickness = 7.dp, shape = CircleShape, hoverDurationMillis = 0, unhoverColor = bg, hoverColor = bg ) } } fun lightScrollColors(): ScrollColors { return ScrollColors( Color(0xFFC9C9C9), ) } fun darkScrollColors(): ScrollColors { return ScrollColors( Color(0xFF494949) ) } val LocalScrollColors = compositionLocalOf { lightScrollColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/SelectionColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class SelectionColors( active: Color, inactive: Color, hover: Color, lightActive: Color, lightInactive: Color, completionPopup: Color, ) { var active by mutableStateOf(active) var inactive by mutableStateOf(inactive) var hover by mutableStateOf(hover) var lightActive by mutableStateOf(lightActive) var lightInactive by mutableStateOf(lightInactive) var completionPopup by mutableStateOf(completionPopup) fun copy( active: Color = this.active, inactive: Color = this.inactive, hover: Color = this.hover, lightActive: Color = this.lightActive, lightInactive: Color = this.lightInactive, completionPopup: Color = this.completionPopup, ): SelectionColors { return SelectionColors(active, inactive, hover, lightActive, lightInactive, completionPopup) } } fun lightSelectionColors(): SelectionColors { return SelectionColors( Color(0xFF2675BF), Color(0xFFD5D5D5), Color(0xFFEDF5FC), Color(0xFFEDF6FE), Color(0xFFF5F5F5), Color(0xFFC5DFFC), ) } fun darkSelectionColors(): SelectionColors { return SelectionColors( Color(0xFF2F65CA), Color(0xFF0D293E), Color(0xFF464A4D), Color(0xFF464A4D), Color(0xFF35383B), Color(0xFF113A5C), ) } val LocalSelectionColors = compositionLocalOf { lightSelectionColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/TabColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class TabColors( selection: Color, focus: Color, selectionInactive: Color, hover: Color, selectionDisabled: Color, bgSelected: Color, ) { var selection by mutableStateOf(selection) var focus by mutableStateOf(focus) var selectionInactive by mutableStateOf(selectionInactive) var hover by mutableStateOf(hover) var selectionDisabled by mutableStateOf(selectionDisabled) var bgSelected by mutableStateOf(bgSelected) fun copy( selection: Color = this.selection, focus: Color = this.focus, selectionInactive: Color = this.selectionInactive, hover: Color = this.hover, selectionDisabled: Color = this.selectionDisabled, bgSelected: Color = this.bgSelected, ): TabColors { return TabColors(selection, focus, selectionInactive, hover, selectionDisabled, bgSelected) } } fun lightTabColors(): TabColors { return TabColors( Color(0xFF4083C9), Color(0xFFDAE4ED), Color(0xFF9CA7B8), Color(0x19000000), Color(0xFFABABAB), Color(0xFFFFFFFF), ) } fun darkTabColors(): TabColors { return TabColors( Color(0xFF4A88C7), Color(0xFF3D4B5C), Color(0xFF747A80), Color(0xFF2E3133), Color(0xFF595959), Color(0xFF4E5254), ) } val LocalTabColors = compositionLocalOf { lightTabColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/TableColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class TableColors( bg: Color, headerBorder: Color, outerBorder: Color, ) { var bg by mutableStateOf(bg) var headerBorder by mutableStateOf(headerBorder) var outerBorder by mutableStateOf(outerBorder) } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/TextColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class TextColors( default: Color, disabled: Color, white: Color, link: Color, infoPanel: Color, infoInput: Color, error: Color, success: Color, ) { var default by mutableStateOf(default) var disabled by mutableStateOf(disabled) var white by mutableStateOf(white) var link by mutableStateOf(link) var infoPanel by mutableStateOf(infoPanel) var infoInput by mutableStateOf(infoInput) var error by mutableStateOf(error) var success by mutableStateOf(success) fun copy( default: Color = this.default, disabled: Color = this.disabled, white: Color = this.white, link: Color = this.link, infoPanel: Color = this.infoPanel, infoInput: Color = this.infoInput, error: Color = this.error, success: Color = this.success, ): TextColors { return TextColors(default, disabled, white, link, infoPanel, infoInput, error, success) } } fun lightTextColors(): TextColors { return TextColors( Color.Black, Color(0xFF8C8C8C), Color.White, Color(0xFF2470B3), Color(0xFF808080), Color(0xFF999999), Color(0xFFC7222D), Color(0xFF368746), ) } fun darkTextColors(): TextColors { return TextColors( Color(0xFFBBBBBB), Color(0xFF777777), Color(0xFFFEFEFE), Color(0xFF589DF6), Color(0xFF8C8C8C), Color(0xFF787878), Color(0xFFFF5261), Color(0xFF50A661), ) } val LocalTextColors = compositionLocalOf { lightTextColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ToggleColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class ToggleColors( bg: Color, off: Color, on: Color, ) { var bg by mutableStateOf(bg) var off by mutableStateOf(off) var on by mutableStateOf(on) } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/color/ToolBarColors.kt ================================================ package io.kanro.compose.jetbrains.color import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class ToolBarColors( buttonPressed: Color, buttonHover: Color, iconSplitBorder: Color, ) { var buttonPressed by mutableStateOf(buttonPressed) var buttonHover by mutableStateOf(buttonHover) var iconSplitBorder by mutableStateOf(iconSplitBorder) fun copy( buttonPressed: Color = this.buttonPressed, buttonHover: Color = this.buttonHover, iconSplitBorder: Color = this.iconSplitBorder, ): ToolBarColors { return ToolBarColors(buttonPressed, buttonHover, iconSplitBorder) } @Composable fun actionIconBgColor(interactionSource: InteractionSource): State { val pressed by interactionSource.collectIsPressedAsState() val hover by interactionSource.collectIsHoveredAsState() val targetValue = when { pressed -> buttonPressed hover -> buttonHover else -> Color.Transparent } return rememberUpdatedState(targetValue) } } fun lightToolBarColors(): ToolBarColors { return ToolBarColors( Color(0xFFCFCFCF), Color(0xFFDFDFDF), Color(0xFFB3B3B3), ) } fun darkToolBarColors(): ToolBarColors { return ToolBarColors( Color(0xFF5C6164), Color(0xFF4C5052), Color(0xFF6B6B6B), ) } val LocalToolBarColors = compositionLocalOf { lightToolBarColors() } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ActionButton.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme object ActionButtonIndication : Indication { private class ActionButtonIndicationInstance( private val shape: Shape, private val isHover: State, private val isPressed: State, private val hoverColor: Color, private val pressedColor: Color, ) : IndicationInstance { override fun ContentDrawScope.drawIndication() { when { isPressed.value -> { val outline = shape.createOutline(size, layoutDirection, this) drawOutline(outline, pressedColor) } isHover.value -> { val outline = shape.createOutline(size, layoutDirection, this) drawOutline(outline, hoverColor) } } drawContent() } } @Composable override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { val shape = remember { RoundedCornerShape(3.dp) } val isPressed = interactionSource.collectIsPressedAsState() val isHover = interactionSource.collectIsHoveredAsState() val hoverColor = JBTheme.toolBarColors.buttonHover val pressedColor = JBTheme.toolBarColors.buttonPressed return remember(JBTheme.toolBarColors, interactionSource) { ActionButtonIndicationInstance( shape, isHover, isPressed, hoverColor, pressedColor ) } } } object ActionButtonDefaults { private val ButtonHorizontalPadding = 3.dp private val ButtonVerticalPadding = 3.dp val MinWidth = 22.dp val MinHeight = 22.dp val IconSize = 16.dp val ContentPadding = PaddingValues( start = ButtonHorizontalPadding, top = ButtonVerticalPadding, end = ButtonHorizontalPadding, bottom = ButtonVerticalPadding ) } @Composable fun ActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, background: Color = Color.Transparent, indication: Indication? = ActionButtonIndication, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(3.dp), border: BorderStroke? = null, contentPadding: PaddingValues = ActionButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { Box( modifier .then(if (border != null) Modifier.border(border, shape) else Modifier) .background( color = background, shape = shape ) .clip(shape) .clickable( interactionSource = interactionSource, indication = indication, enabled = enabled, onClick = onClick ) .hoverable(interactionSource, enabled), propagateMinConstraints = true ) { CompositionLocalProvider( LocalContentColor provides (if (!enabled) JBTheme.iconColors.disabled else LocalContentColor.current) ) { Row( Modifier .defaultMinSize( minWidth = ActionButtonDefaults.MinWidth, minHeight = ActionButtonDefaults.MinHeight ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) } } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Button.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified import io.kanro.compose.jetbrains.JBTheme @Stable interface ButtonStyle { @Composable fun borderBrush(enabled: Boolean): State @Composable fun backgroundBrush(enabled: Boolean): State @Composable fun contentColor(enabled: Boolean): State @Composable fun focusingColor(enabled: Boolean, interactionSource: InteractionSource): State } private data class DefaultButtonStyle( private val borderBrush: Brush, private val backgroundBrush: Brush, private val contentColor: Color, private val disableBorderBrush: Brush, private val disableBackgroundBrush: Brush, private val disabledContentColor: Color, private val focusingColor: Color, ) : ButtonStyle { @Composable override fun borderBrush(enabled: Boolean): State { return rememberUpdatedState(if (enabled) borderBrush else disableBorderBrush) } @Composable override fun backgroundBrush(enabled: Boolean): State { return rememberUpdatedState(if (enabled) backgroundBrush else disableBackgroundBrush) } @Composable override fun contentColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) contentColor else disabledContentColor) } @Composable override fun focusingColor(enabled: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() return rememberUpdatedState(if (enabled && focused) focusingColor else Color.Transparent) } } object ButtonDefaults { private val ButtonHorizontalPadding = 14.dp private val ButtonVerticalPadding = 3.dp val ContentPadding = PaddingValues( start = ButtonHorizontalPadding, top = ButtonVerticalPadding, end = ButtonHorizontalPadding, bottom = ButtonVerticalPadding ) val MinWidth = 72.dp val MinHeight = 24.dp val IconSize = 16.dp val IconSpacing = 8.dp @Composable fun buttonStyle( borderBrush: Brush = Brush.verticalGradient( listOf( JBTheme.buttonColors.borderDefaultStart, JBTheme.buttonColors.borderDefaultEnd ) ), backgroundBrush: Brush = Brush.verticalGradient( listOf( JBTheme.buttonColors.defaultStart, JBTheme.buttonColors.defaultEnd ) ), contentColor: Color = JBTheme.contentColorFor(JBTheme.buttonColors.defaultEnd), disableBorderBrush: Brush = SolidColor(JBTheme.buttonColors.borderDisabled), disableBackgroundBrush: Brush = SolidColor(JBTheme.buttonColors.bgDisabled), disabledContentColor: Color = JBTheme.textColors.disabled, focusingColor: Color = JBTheme.focusColors.default, ): ButtonStyle = DefaultButtonStyle( borderBrush, backgroundBrush, contentColor, disableBorderBrush, disableBackgroundBrush, disabledContentColor, focusingColor ) @Composable fun outlineButtonStyle( borderBrush: Brush = SolidColor(JBTheme.buttonColors.border), backgroundBrush: Brush = SolidColor(JBTheme.buttonColors.bg), contentColor: Color = JBTheme.contentColorFor(JBTheme.buttonColors.bg), disableBorderBrush: Brush = SolidColor(JBTheme.buttonColors.borderDisabled), disableBackgroundBrush: Brush = SolidColor(JBTheme.buttonColors.bgDisabled), disabledContentColor: Color = JBTheme.textColors.disabled, focusingColor: Color = JBTheme.focusColors.default, ): ButtonStyle = DefaultButtonStyle( borderBrush, backgroundBrush, contentColor, disableBorderBrush, disableBackgroundBrush, disabledContentColor, focusingColor ) } @Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(3.dp), borderWidth: Dp = 1.dp, style: ButtonStyle = ButtonDefaults.buttonStyle(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { val focusRequester = remember { FocusRequester() } Box( modifier .then( if (borderWidth.isSpecified) Modifier.border( borderWidth, style.borderBrush(enabled).value, shape ) else Modifier ) .background( brush = style.backgroundBrush(enabled).value, shape = shape ) .focusRequester(focusRequester) .focusable(enabled, interactionSource) .clickable( interactionSource = interactionSource, indication = null, enabled = enabled, onClick = onClick ) .pointerInput("Fo") { this.awaitPointerEventScope { var event: PointerEvent do { event = awaitPointerEvent() if (event.changes.any { it.changedToDownIgnoreConsumed() }) { focusRequester.requestFocus() } } while (true) } } .buttonIndicator(style.focusingColor(enabled, interactionSource).value, shape, 2.dp, 5.dp), propagateMinConstraints = true ) { val contentColor by style.contentColor(enabled) CompositionLocalProvider( LocalContentAlpha provides contentColor.alpha, LocalContentColor provides contentColor ) { ProvideTextStyle(JBTheme.typography.defaultBold) { Row( Modifier .defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) } } } } private fun Modifier.buttonIndicator(color: Color, shape: Shape, width: Dp, cornerRadius: Dp): Modifier { if (color.alpha == 0f) return this return drawBehind { val controlOutline = shape.createOutline(size, layoutDirection, this) val highlightOutline = RoundRect(controlOutline.bounds.inflate(width.toPx()), CornerRadius(cornerRadius.toPx())) val highlightPath = Path().apply { this.fillType = PathFillType.EvenOdd addRoundRect(highlightOutline) addOutline(controlOutline) close() } drawPath(highlightPath, color) } } @Composable fun OutlineButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(3.dp), borderWidth: Dp = 1.dp, style: ButtonStyle = ButtonDefaults.outlineButtonStyle(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) = Button(onClick, modifier, enabled, interactionSource, shape, borderWidth, style, contentPadding, content) ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/CheckBox.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.Canvas import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.triStateToggleable import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme import io.kanro.compose.jetbrains.icons.JBIcons import io.kanro.compose.jetbrains.icons.jbicons.Actions import io.kanro.compose.jetbrains.icons.jbicons.actions.Checkmark import io.kanro.compose.jetbrains.icons.jbicons.actions.CheckmarkIndeterminate @Composable fun CheckBox( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { TriStateCheckbox( state = ToggleableState(checked), onClick = if (onCheckedChange != null) { { onCheckedChange(!checked) } } else null, interactionSource = interactionSource, enabled = enabled, modifier = modifier ) } @Composable fun TriStateCheckbox( state: ToggleableState, onClick: (() -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { val toggleableModifier = if (onClick != null) { Modifier.triStateToggleable( state = state, onClick = onClick, enabled = enabled, role = Role.Checkbox, interactionSource = interactionSource, indication = null ) } else { Modifier } CheckboxImpl( enabled = enabled, value = state, modifier = modifier .then(toggleableModifier), ) } @Composable private fun CheckboxImpl( enabled: Boolean, value: ToggleableState, modifier: Modifier, ) { val icon = when (value) { ToggleableState.On -> rememberVectorPainter(JBIcons.Actions.Checkmark) ToggleableState.Indeterminate -> rememberVectorPainter(JBIcons.Actions.CheckmarkIndeterminate) else -> null } val iconFilter = if (!enabled) { ColorFilter.tint(JBTheme.iconColors.disabled, BlendMode.DstIn) } else null val bg = if (enabled) { when (value) { ToggleableState.On, ToggleableState.Indeterminate -> JBTheme.checkBoxColors.bgSelected ToggleableState.Off -> JBTheme.checkBoxColors.bg } } else JBTheme.checkBoxColors.bgDisabled val border = if (enabled) { when (value) { ToggleableState.On, ToggleableState.Indeterminate -> JBTheme.checkBoxColors.borderSelected ToggleableState.Off -> JBTheme.checkBoxColors.border } } else JBTheme.checkBoxColors.borderDisabled Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(14.dp)) { drawRoundRect(border, cornerRadius = CornerRadius(2.dp.toPx())) drawRoundRect( bg, size = Size(12.dp.toPx(), 12.dp.toPx()), topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), cornerRadius = CornerRadius(1.dp.toPx()) ) if (icon != null) { with(icon) { 14.dp.toPx() draw(Size(14.dp.toPx(), 14.dp.toPx()), colorFilter = iconFilter) } } } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ComboBox.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBThemeStyle import io.kanro.compose.jetbrains.LocalIconTheme import io.kanro.compose.jetbrains.icons.JBIcons import io.kanro.compose.jetbrains.icons.jbicons.General import io.kanro.compose.jetbrains.icons.jbicons.general.ButtonDropTriangle import io.kanro.compose.jetbrains.icons.jbicons.general.ButtonDropTriangleDark @Composable fun DropdownList( items: List, value: T, onValueChange: ((T) -> Unit)? = null, modifier: Modifier = Modifier, enabled: Boolean = true, valueRender: (T) -> String = { it.toString() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, style: TextFieldStyle = TextFieldDefaults.textFieldStyle(), ) { val shape = RoundedCornerShape(3.dp) val focusRequester = remember { FocusRequester() } val expanded = remember { mutableStateOf(false) } Box( modifier.border(1.dp, style.borderColor(enabled, false, interactionSource).value, shape).height(24.dp) .background(style.backgroundColor(enabled, interactionSource).value).focusable(enabled, interactionSource) .focusRequester(focusRequester) .clickable(interactionSource = interactionSource, indication = null, enabled = enabled, onClick = { expanded.value = true focusRequester.requestFocus() }).comboBoxIndicator(style.indicatorColor(false, interactionSource).value, shape, 2.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.width(6.dp)) Text( valueRender(value), Modifier.defaultMinSize(50.dp), color = style.textColor(enabled, false, interactionSource).value ) Spacer(Modifier.width(1.dp).fillMaxHeight()) Box(Modifier.size(22.dp), contentAlignment = Alignment.Center) { val isDarkTheme = LocalIconTheme.current == JBThemeStyle.DARK Icon( imageVector = if (isDarkTheme) JBIcons.General.ButtonDropTriangleDark else JBIcons.General.ButtonDropTriangle ) } } DropdownMenu(expanded.value, offset = DpOffset(0.dp, 4.dp), onDismissRequest = { expanded.value = false }) { items.forEach { DropdownMenuItem( modifier = Modifier.defaultMinSize(79.dp, 21.dp), onClick = { expanded.value = false onValueChange?.invoke(it) }, enabled = enabled ) { Text(valueRender(it), Modifier.padding(horizontal = 6.dp)) } } } } } private fun Modifier.comboBoxIndicator(color: Color, shape: Shape, width: Dp): Modifier { if (color.alpha == 0f) return this return drawBehind { val controlOutline = shape.createOutline(size, layoutDirection, this) val highlightPath = Path().apply { this.fillType = PathFillType.EvenOdd addOutline(controlOutline) val borderRect = controlOutline.bounds.inflate(width.toPx()) addRoundRect(RoundRect(borderRect, 4.dp.toPx(), 4.dp.toPx())) close() } drawPath(highlightPath, color) } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ContentAlpha.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.runtime.compositionLocalOf val LocalContentAlpha = compositionLocalOf { 1f } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ContentColor.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color import io.kanro.compose.jetbrains.JBTheme val LocalContentColor = compositionLocalOf { Color.Unspecified } @Composable fun JBTheme.contentColorFor(backgroundColor: Color): Color { return if ((backgroundColor.green + backgroundColor.blue + backgroundColor.red) / 3.0 > 0.6) { textColors.default } else { textColors.white } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ContextMenu.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.ContextMenuData import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuRepresentation import androidx.compose.foundation.ContextMenuState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.isUnspecified import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.changedToDown import androidx.compose.ui.input.pointer.isSecondaryPressed import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.rememberCursorPositionProvider import io.kanro.compose.jetbrains.JBTheme private val LocalContextMenuData = staticCompositionLocalOf { null } @Composable internal fun ContextMenuDataProvider( data: ContextMenuData, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalContextMenuData provides data ) { content() } } private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent { var event: PointerEvent do { event = awaitPointerEvent() } while (!event.changes.all { it.changedToDown() }) return event } private fun Modifier.contextMenuDetector( state: ContextMenuState, enabled: Boolean = true, ): Modifier { return if (enabled && state.status == ContextMenuState.Status.Closed) { this.pointerInput(state) { forEachGesture { awaitPointerEventScope { val event = awaitEventFirstDown() if (event.buttons.isSecondaryPressed) { event.changes.forEach { if (it.pressed != it.previousPressed) it.consume() } state.status = ContextMenuState.Status.Open(Rect(event.changes[0].position, 0f)) } } } } } else { Modifier } } val LocalContextMenuRepresentation: ProvidableCompositionLocal = staticCompositionLocalOf { JBContextMenuRepresentation(Color.Unspecified, Color.Unspecified) } @Composable fun ContextMenuArea( items: () -> List, state: ContextMenuState = remember { ContextMenuState() }, enabled: Boolean = true, content: @Composable () -> Unit, ) { val data = ContextMenuData(items, LocalContextMenuData.current) ContextMenuDataProvider(data) { Box(Modifier.contextMenuDetector(state, enabled), propagateMinConstraints = true) { content() } LocalContextMenuRepresentation.current.Representation(state, items) } } @OptIn(ExperimentalComposeUiApi::class) class JBContextMenuRepresentation( private val backgroundColor: Color, private val borderColor: Color, ) : ContextMenuRepresentation { @Composable override fun Representation(state: ContextMenuState, items: () -> List) { val isOpen = state.status is ContextMenuState.Status.Open if (isOpen) { Popup( focusable = true, onDismissRequest = { state.status = ContextMenuState.Status.Closed }, popupPositionProvider = rememberCursorPositionProvider(), onKeyEvent = { if (it.key == Key.Escape) { state.status = ContextMenuState.Status.Closed true } else { false } }, ) { val backgroundColor = if (backgroundColor.isUnspecified) { JBTheme.panelColors.bgContent } else backgroundColor val borderColor = if (borderColor.isUnspecified) { JBTheme.panelColors.border } else borderColor Column( Modifier.background(backgroundColor).border(1.dp, borderColor).width(IntrinsicSize.Max) ) { items().forEach { DropdownMenuItem( modifier = Modifier.defaultMinSize(79.dp, 21.dp), onClick = { it.onClick() state.status = ContextMenuState.Status.Closed }, ) { Text(it.label, Modifier.padding(horizontal = 6.dp)) } } } } } } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/DropdownMenu.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import io.kanro.compose.jetbrains.JBTheme import io.kanro.compose.jetbrains.color.LocalPanelColors @OptIn(ExperimentalComposeUiApi::class) @Composable fun DropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, focusable: Boolean = true, modifier: Modifier = Modifier, offset: DpOffset = DpOffset(0.dp, 0.dp), content: @Composable ColumnScope.() -> Unit, ) { if (expanded) { val density = LocalDensity.current val popupPositionProvider = DropdownMenuPositionProvider( offset, density ) Popup( focusable = focusable, onDismissRequest = onDismissRequest, popupPositionProvider = popupPositionProvider, onKeyEvent = { if (it.key == Key.Escape) { onDismissRequest() true } else { false } }, ) { val panelColor = LocalPanelColors.current Column(modifier.background(panelColor.bgContent).border(1.dp, panelColor.border).width(IntrinsicSize.Max)) { content() } } } } @Composable fun DropdownMenuItem( modifier: Modifier = Modifier, onClick: () -> Unit, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable () -> Unit, ) { Box( modifier .clickable( interactionSource = interactionSource, indication = DropdownMenuItemHoverIndication, enabled = enabled, onClick = onClick ) .fillMaxWidth() .hoverable(interactionSource, enabled), contentAlignment = Alignment.CenterStart ) { val isHovered = interactionSource.collectIsHoveredAsState() SelectionScope(isHovered.value) { content() } } } object DropdownMenuItemHoverIndication : Indication { private class HoverIndicationInstance( private val isHover: State, private val hoverColor: Color, ) : IndicationInstance { override fun ContentDrawScope.drawIndication() { when { isHover.value -> { drawRect(hoverColor, size = size) } } drawContent() } } @Composable override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { val isHover = interactionSource.collectIsHoveredAsState() val hoverColor = JBTheme.selectionColors.active return remember(JBTheme.selectionColors, interactionSource) { HoverIndicationInstance( isHover, hoverColor, ) } } } data class DropdownMenuPositionProvider( val contentOffset: DpOffset, val density: Density, val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }, ) : PopupPositionProvider { override fun calculatePosition( anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize, ): IntOffset { // The min margin above and below the menu, relative to the screen. val verticalMargin = with(density) { 0 } // The content offset specified using the dropdown offset parameter. val contentOffsetX = with(density) { contentOffset.x.roundToPx() } val contentOffsetY = with(density) { contentOffset.y.roundToPx() } // Compute horizontal position. val toRight = anchorBounds.left + contentOffsetX val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width val toDisplayRight = windowSize.width - popupContentSize.width val toDisplayLeft = 0 val x = if (layoutDirection == LayoutDirection.Ltr) { sequenceOf(toRight, toLeft, toDisplayRight) } else { sequenceOf(toLeft, toRight, toDisplayLeft) }.firstOrNull { it >= 0 && it + popupContentSize.width <= windowSize.width } ?: toLeft // Compute vertical position. val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height val toCenter = anchorBounds.top - popupContentSize.height / 2 val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin var y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { it >= verticalMargin && it + popupContentSize.height <= windowSize.height - verticalMargin } ?: toTop // Desktop specific vertical position checking val aboveAnchor = anchorBounds.top + contentOffsetY val belowAnchor = windowSize.height - anchorBounds.bottom - contentOffsetY if (belowAnchor >= aboveAnchor) { y = anchorBounds.bottom + contentOffsetY } if (y + popupContentSize.height > windowSize.height) { y = windowSize.height - popupContentSize.height } if (y < 0) { y = 0 } onPositionCalculated( anchorBounds, IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height) ) return IntOffset(x, y) } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/EmbeddedTextField.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable fun EmbeddedTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = true, maxLines: Int = 1, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RectangleShape, style: TextFieldStyle = TextFieldDefaults.textFieldStyle(), ) { TextField( value, onValueChange, modifier.embeddedTextFieldIndicator(style.indicatorColor(isError, interactionSource).value, shape, 2.dp), enabled, readOnly, textStyle, placeholder, leadingIcon, trailingIcon, isError, visualTransformation, keyboardOptions, keyboardActions, singleLine, maxLines, interactionSource, shape, InnerStyle(style) ) } private class InnerStyle(val style: TextFieldStyle) : TextFieldStyle { @Composable override fun textColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() return if (focused) style.textColor( enabled, isError, interactionSource ) else rememberUpdatedState(Color.Unspecified) } @Composable override fun backgroundColor(enabled: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() return if (focused) style.backgroundColor( enabled, interactionSource ) else rememberUpdatedState(Color.Transparent) } @Composable override fun placeholderColor(enabled: Boolean): State { return style.placeholderColor(enabled) } @Composable override fun borderColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State { return rememberUpdatedState(Color.Transparent) } @Composable override fun indicatorColor(isError: Boolean, interactionSource: InteractionSource): State { return rememberUpdatedState(Color.Transparent) } @Composable override fun cursorColor(isError: Boolean): State { return style.cursorColor(isError) } } private fun Modifier.embeddedTextFieldIndicator(color: Color, shape: Shape, width: Dp): Modifier { if (color.alpha == 0f) return this return drawBehind { val controlOutline = shape.createOutline(size, layoutDirection, this) val highlightPath = Path().apply { this.fillType = PathFillType.EvenOdd addOutline(controlOutline) addRect(controlOutline.bounds.deflate(width.toPx())) close() } drawPath(highlightPath, color) } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Icon.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toolingGraphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBThemeStyle import io.kanro.compose.jetbrains.LocalIconTheme @Composable fun Icon( resource: String, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, ) { Icon( themedSvgResource(resource, LocalIconTheme.current), contentDescription, modifier, colorFilter = colorFilter, ) } @Composable fun Icon( bitmap: ImageBitmap, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, ) { val painter = remember(bitmap) { BitmapPainter(bitmap) } Icon( painter = painter, contentDescription = contentDescription, modifier = modifier, colorFilter = colorFilter, ) } @Composable fun Icon( imageVector: ImageVector, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, ) { Icon( painter = rememberVectorPainter(imageVector), contentDescription = contentDescription, modifier = modifier, colorFilter = colorFilter, ) } @Composable fun Icon( painter: Painter, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, ) { val semantics = if (contentDescription != null) { Modifier.semantics { this.contentDescription = contentDescription this.role = Role.Image } } else { Modifier } val filter = colorFilter ?: run { if (LocalContentColor.current.isSpecified) { ColorFilter.tint(LocalContentColor.current.copy(alpha = LocalContentAlpha.current)) } else { null } } Box( modifier.toolingGraphicsLayer().defaultSizeFor(painter) .paint( painter, contentScale = ContentScale.None, colorFilter = filter ) .then(semantics) ) } @Composable fun themedSvgResource(resource: String, theme: JBThemeStyle = LocalIconTheme.current): Painter { var realResource = resource if (theme == JBThemeStyle.DARK) { if (!realResource.endsWith("_dark.svg")) { val dark = realResource.replace(".svg", "_dark.svg") if (Thread.currentThread().contextClassLoader.getResource(dark) != null) { realResource = dark } } } else { if (realResource.endsWith("_dark.svg")) { val light = realResource.replace("_dark.svg", ".svg") if (Thread.currentThread().contextClassLoader.getResource(light) != null) { realResource = light } } } return painterResource(realResource) } private fun Modifier.defaultSizeFor(painter: Painter) = this.then( if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) { DefaultIconSizeModifier } else { Modifier } ) private fun Size.isInfinite() = width.isInfinite() && height.isInfinite() private val DefaultIconSizeModifier = Modifier.size(16.dp) ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/JBToolBar.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme @Composable fun JBToolBar( orientation: Orientation, arrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(8.dp), modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalToolBarOrientation provides orientation, ) { when (orientation) { Orientation.Vertical -> JBToolBarColumn( modifier.width(28.dp), verticalArrangement = arrangement, content = content ) Orientation.Horizontal -> JBToolBarRow( modifier.height(28.dp), horizontalArrangement = arrangement, content = content ) } } } @Composable private fun JBToolBarColumn( modifier: Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), content: @Composable () -> Unit, ) { Column( modifier, verticalArrangement, Alignment.CenterHorizontally, ) { content() } } @Composable private fun JBToolBarRow( modifier: Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), content: @Composable () -> Unit, ) { Row( modifier, horizontalArrangement, Alignment.CenterVertically, ) { content() } } val LocalToolBarOrientation = compositionLocalOf { Orientation.Horizontal } @Composable fun ToolBarSeparator(modifier: Modifier = Modifier, color: Color = JBTheme.toolBarColors.iconSplitBorder) { val orientation = LocalToolBarOrientation.current Spacer( modifier = modifier.run { when (orientation) { Orientation.Vertical -> size(20.dp, 1.dp) Orientation.Horizontal -> size(1.dp, 20.dp) } }.background(color) ) } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/JBTree.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme import io.kanro.compose.jetbrains.JBThemeStyle import io.kanro.compose.jetbrains.LocalIconTheme import io.kanro.compose.jetbrains.icons.JBIcons import io.kanro.compose.jetbrains.icons.jbicons.Actions import io.kanro.compose.jetbrains.icons.jbicons.actions.ArrowExpand import io.kanro.compose.jetbrains.icons.jbicons.actions.ArrowExpandDark @Composable fun JBTreeItem( modifier: Modifier = Modifier, selected: Boolean, onClick: () -> Unit, content: @Composable () -> Unit, ) { JBTreeBasicItem(modifier, selected, onClick) { Box(Modifier.padding(start = 16.dp)) { content() } } } @Composable fun JBTreeItem( modifier: Modifier = Modifier, selected: Boolean, onClick: () -> Unit, expanded: Boolean, expanding: (Boolean) -> Unit, content: @Composable () -> Unit, children: @Composable () -> Unit, ) { Column(modifier) { JBTreeBasicItem( modifier, selected, onClick = onClick, onDoubleClick = { expanding(!expanded) onClick() } ) { Row { val isDarkTheme = LocalIconTheme.current == JBThemeStyle.DARK Icon( imageVector = if (isDarkTheme) JBIcons.Actions.ArrowExpandDark else JBIcons.Actions.ArrowExpand, modifier = Modifier.size(16.dp).clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { expanding(!expanded) onClick() }.graphicsLayer(rotationZ = if (expanded) 90f else 0f).align(Alignment.CenterVertically) ) Box { content() } } } if (expanded) { CompositionLocalProvider( LocalTreeLevel provides LocalTreeLevel.current + 1, content = children ) } } } @Composable @OptIn(ExperimentalFoundationApi::class) internal fun JBTreeBasicItem( modifier: Modifier = Modifier, selected: Boolean = false, onClick: () -> Unit, onDoubleClick: (() -> Unit)? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable () -> Unit, ) { val padding = 7 + (LocalTreeLevel.current - 1) * 18 Box( Modifier.combinedClickable( interactionSource, indication = ListItemHoverIndication, onDoubleClick = onDoubleClick, onClick = onClick ).then(modifier) .height(20.dp) .run { if (selected) { background(color = JBTheme.selectionColors.active) } else { this } }.hoverable(interactionSource), ) { SelectionScope(selected) { Box(Modifier.padding(start = padding.dp)) { content() } } } } @Composable fun JBTreeList( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Column(modifier) { content() } } val LocalTreeLevel: ProvidableCompositionLocal = compositionLocalOf { 1 } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/JPanel.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme @Composable fun JPanel( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { Box(modifier.background(JBTheme.panelColors.bgDialog)) { content() } } @Composable fun JPanelBorder(modifier: Modifier = Modifier) { Spacer(modifier.background(JBTheme.panelColors.border)) } fun Modifier.jBorder(all: Dp = 0.dp, color: Color): Modifier { return jBorder(all, all, all, all, color) } fun Modifier.jBorder( horizontal: Dp = 0.dp, vertical: Dp = 0.dp, color: Color, ): Modifier { return jBorder(horizontal, horizontal, vertical, vertical, color) } fun Modifier.jBorder( start: Dp = 0.dp, end: Dp = 0.dp, top: Dp = 0.dp, bottom: Dp = 0.dp, color: Color, ): Modifier { return drawWithCache { onDrawWithContent { drawContent() var rect = Rect(Offset.Zero, size) if (start.roundToPx() > 0) { drawRect(color, rect.topLeft, Size(start.toPx(), rect.height)) rect = Rect(rect.left + start.roundToPx(), rect.top, rect.right, rect.bottom) } if (end.roundToPx() > 0) { drawRect(color, Offset(rect.right - end.toPx(), rect.top), Size(end.toPx(), rect.height)) rect = Rect(rect.left, rect.top, rect.right - end.roundToPx(), rect.bottom) } if (top.roundToPx() > 0) { drawRect(color, rect.topLeft, Size(rect.width, top.toPx())) rect = Rect(rect.left, rect.top + top.roundToPx(), rect.right, rect.bottom) } if (bottom.roundToPx() > 0) { drawRect(color, Offset(rect.left, rect.bottom - bottom.toPx()), Size(rect.width, bottom.toPx())) } } } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ListView.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import io.kanro.compose.jetbrains.JBTheme object ListItemHoverIndication : Indication { private class HoverIndicationInstance( private val isHover: State, private val hoverColor: Color, ) : IndicationInstance { override fun ContentDrawScope.drawIndication() { if (isHover.value) { drawRect(hoverColor, size = size) } drawContent() } } @Composable override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { val isHover = interactionSource.collectIsHoveredAsState() val hoverColor = JBTheme.selectionColors.hover return remember(JBTheme.selectionColors, interactionSource) { HoverIndicationInstance( isHover, hoverColor ) } } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/ProgressBar.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.Canvas import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme @Composable fun ProgressBar( progress: Float, modifier: Modifier = Modifier, ) { val bgColor = JBTheme.progressColors.bg val progressColor = JBTheme.progressColors.progress Canvas( modifier .progressSemantics(progress) .size(200.dp, 4.dp) .focusable() ) { val strokeWidth = size.height val length = size.width drawLine( bgColor, Offset(0f, strokeWidth / 2f), Offset(length, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) drawLine( progressColor, Offset(0f, strokeWidth / 2f), Offset(length * progress, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) } } @Composable fun ProgressBar( modifier: Modifier = Modifier, ) { val transition = rememberInfiniteTransition() val currentOffset by transition.animateFloat( 0f, 1f, infiniteRepeatable( animation = keyframes { durationMillis = 1000 } ) ) val progressColor = JBTheme.progressColors.progress Canvas( modifier .progressSemantics() .size(200.dp, 4.dp) .focusable() ) { val strokeWidth = size.height val length = size.width val offset = currentOffset * 80f val brush = Brush.linearGradient( listOf( Color(0x00FFFFFF), Color(0x7FFFFFFF), Color(0x00FFFFFF) ), start = Offset(offset, 0f), end = Offset(offset + 80f, 0f), tileMode = TileMode.Repeated ) drawLine( progressColor, Offset(0f, strokeWidth / 2f), Offset(length, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) drawLine( brush, Offset(0f, strokeWidth / 2f), Offset(length, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Selection.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.selection.selectable import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import io.kanro.compose.jetbrains.JBTheme import io.kanro.compose.jetbrains.JBThemeStyle import io.kanro.compose.jetbrains.LocalIconTheme import io.kanro.compose.jetbrains.LocalSelectionScope import io.kanro.compose.jetbrains.color.LocalTextColors val emptySelectionScope = emptyArray>() val lightSelectionScope: @Composable () -> Array> = { arrayOf( LocalIconTheme provides JBThemeStyle.DARK, LocalTextColors provides JBTheme.textColors.copy( infoInput = Color.White ), LocalContentColor provides Color.White, LocalContentAlpha provides 1.0f, LocalContextMenuRepresentation provides JBContextMenuRepresentation( JBTheme.panelColors.bgContent, JBTheme.panelColors.border ) ) } val darkSelectionScope: @Composable () -> Array> = { arrayOf( LocalIconTheme provides JBThemeStyle.DARK, LocalTextColors provides JBTheme.textColors.copy( infoInput = Color.White ), LocalContentColor provides Color.White, LocalContentAlpha provides 1.0f, LocalContextMenuRepresentation provides JBContextMenuRepresentation( JBTheme.panelColors.bgContent, JBTheme.panelColors.border ) ) } @Composable fun SelectionScope(selected: Boolean, block: @Composable () -> Unit) { CompositionLocalProvider(* if (selected) LocalSelectionScope.current() else emptySelectionScope) { block() } } @Composable fun SelectionRow( selected: Boolean, modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, role: Role? = null, onClick: () -> Unit, content: @Composable RowScope.() -> Unit, ) { SelectionScope(selected) { val selectedColor = JBTheme.selectionColors.active Row( modifier = modifier.background(color = JBTheme.panelColors.bgContent).selectable( selected = selected, interactionSource = interactionSource, indication = ListItemHoverIndication, onClick = onClick, role = role ).drawWithContent { if (selected) { drawRect(selectedColor, size = size) } drawContent() }, horizontalArrangement = horizontalArrangement, verticalAlignment = verticalAlignment, content = content ) } } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Tab.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.Indication import androidx.compose.foundation.IndicationInstance import androidx.compose.foundation.background import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.height import androidx.compose.foundation.selection.selectable import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.JBTheme object TabIndication : Indication { private class TabIndicationInstance( private val isHover: State, private val hoverColor: Color, ) : IndicationInstance { override fun ContentDrawScope.drawIndication() { if (isHover.value) { drawRect(hoverColor, Offset.Zero, size) } drawContent() } } @Composable override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { val isHover = interactionSource.collectIsHoveredAsState() val hoverColor = JBTheme.tabColors.hover return remember(JBTheme.tabColors, interactionSource) { TabIndicationInstance( isHover, hoverColor, ) } } } @Composable fun Tab( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable BoxScope.() -> Unit, ) { val selectionColor = JBTheme.tabColors.selection Box( modifier .hoverable(interactionSource) .height(28.dp) .run { if (selected) { background(JBTheme.tabColors.bgSelected) } else { this } } .drawWithContent { drawContent() if (selected) { val height = 3.dp.toPx() drawRect( selectionColor, Offset(0f, size.height - height), Size(size.width, height) ) } }.selectable( selected = selected, onClick = onClick, enabled = enabled, role = Role.Tab, interactionSource = interactionSource, indication = TabIndication ), propagateMinConstraints = true, contentAlignment = Alignment.Center, content = content ) } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/Text.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import io.kanro.compose.jetbrains.color.LocalTextColors import io.kanro.compose.jetbrains.color.TextColors @Composable fun Text( text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, textColors: TextColors = LocalTextColors.current, ) { Text( AnnotatedString(text), modifier, color, fontSize, fontStyle, fontWeight, fontFamily, letterSpacing, textDecoration, textAlign, lineHeight, overflow, softWrap, maxLines, emptyMap(), onTextLayout, style, textColors, ) } @Composable fun Text( text: AnnotatedString, modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, textColors: TextColors = LocalTextColors.current, ) { val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.current.takeOrElse { textColors.default }.copy(alpha = LocalContentAlpha.current) } } val mergedStyle = style.merge( TextStyle( color = textColor, fontSize = fontSize, fontWeight = fontWeight, textAlign = textAlign, lineHeight = lineHeight, fontFamily = fontFamily, textDecoration = textDecoration, fontStyle = fontStyle, letterSpacing = letterSpacing, ), ) BasicText( text = text, modifier = modifier, style = mergedStyle, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = 1, inlineContent = inlineContent, ) } val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default } @Composable fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) { val mergedStyle = LocalTextStyle.current.merge(value) CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content) } ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/control/TextField.kt ================================================ package io.kanro.compose.jetbrains.control import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import io.kanro.compose.jetbrains.JBTheme import kotlin.math.max import kotlin.math.roundToInt @Stable interface TextFieldStyle { @Composable fun textColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State @Composable fun backgroundColor(enabled: Boolean, interactionSource: InteractionSource): State @Composable fun placeholderColor(enabled: Boolean): State @Composable fun borderColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State @Composable fun indicatorColor(isError: Boolean, interactionSource: InteractionSource): State @Composable fun cursorColor(isError: Boolean): State } private data class DefaultTextFieldStyle( private val textColor: Color, private val disabledTextColor: Color, private val errorTextColor: Color, private val backgroundColor: Color, private val disabledBackgroundColor: Color, private val placeholderColor: Color, private val disabledPlaceholderColor: Color, private val borderColor: Color, private val disabledBorderColor: Color, private val errorBorderColor: Color, private val focusedBorderColor: Color, private val indicatorColor: Color, private val errorIndicatorColor: Color, private val cursorColor: Color, private val errorCursorColor: Color, ) : TextFieldStyle { @Composable override fun textColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State { return rememberUpdatedState( when { !enabled -> disabledTextColor isError -> errorTextColor else -> textColor } ) } @Composable override fun backgroundColor(enabled: Boolean, interactionSource: InteractionSource): State { return rememberUpdatedState(if (enabled) backgroundColor else disabledBackgroundColor) } @Composable override fun placeholderColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) placeholderColor else disabledPlaceholderColor) } @Composable override fun borderColor(enabled: Boolean, isError: Boolean, interactionSource: InteractionSource): State { val focused by interactionSource.collectIsFocusedAsState() val targetValue = when { !enabled -> disabledBorderColor isError -> errorBorderColor focused -> focusedBorderColor else -> borderColor } return rememberUpdatedState(targetValue) } @Composable override fun indicatorColor( isError: Boolean, interactionSource: InteractionSource, ): State { val focused by interactionSource.collectIsFocusedAsState() val targetValue = when { isError -> errorIndicatorColor focused -> indicatorColor else -> Color.Transparent } return rememberUpdatedState(targetValue) } @Composable override fun cursorColor(isError: Boolean): State { return rememberUpdatedState(if (isError) errorCursorColor else cursorColor) } } @Composable fun TextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = true, maxLines: Int = 1, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RectangleShape, style: TextFieldStyle = TextFieldDefaults.textFieldStyle(), ) { var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) } val textFieldValue = textFieldValueState.copy(text = value) TextField( enabled = enabled, readOnly = readOnly, value = textFieldValue, onValueChange = { textFieldValueState = it if (value != it.text) { onValueChange(it.text) } }, modifier = modifier, singleLine = singleLine, textStyle = textStyle, placeholder = placeholder, leadingIcon = leadingIcon, trailingIcon = trailingIcon, isError = isError, visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, maxLines = maxLines, interactionSource = interactionSource, shape = shape, style = style ) } @Composable fun TextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = true, maxLines: Int = 1, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RectangleShape, style: TextFieldStyle = TextFieldDefaults.textFieldStyle(), ) { val textColor = textStyle.color.takeOrElse { style.textColor(enabled, isError, interactionSource).value }.takeOrElse { LocalContentColor.current.copy(alpha = LocalContentAlpha.current) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) val transformedText = remember(value.annotatedString, visualTransformation) { visualTransformation.filter(value.annotatedString) }.text val decoratedPlaceholder: @Composable ((Modifier) -> Unit)? = if (placeholder != null && transformedText.isEmpty()) { @Composable { _ -> Box { Decoration( contentColor = style.placeholderColor(enabled).value, content = placeholder, typography = JBTheme.typography.default ) } } } else null TextFieldLayout( modifier = modifier, value = value, onValueChange = onValueChange, enabled = enabled, readOnly = readOnly, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, textStyle = mergedTextStyle, singleLine = singleLine, maxLines = maxLines, visualTransformation = visualTransformation, interactionSource = interactionSource, decoratedPlaceholder = decoratedPlaceholder, leading = leadingIcon, trailing = trailingIcon, borderWidth = 1.dp, borderColor = style.borderColor(enabled, isError, interactionSource).value, indicatorWidth = 2.dp, indicatorColor = style.indicatorColor(isError, interactionSource).value, shape = shape, backgroundColor = style.backgroundColor(enabled, interactionSource).value, cursorColor = style.cursorColor(isError).value ) } @Composable internal fun TextFieldLayout( modifier: Modifier, value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, enabled: Boolean, readOnly: Boolean, keyboardOptions: KeyboardOptions, keyboardActions: KeyboardActions, textStyle: TextStyle, singleLine: Boolean, maxLines: Int = Int.MAX_VALUE, visualTransformation: VisualTransformation, interactionSource: MutableInteractionSource, decoratedPlaceholder: @Composable ((Modifier) -> Unit)?, leading: @Composable (() -> Unit)?, trailing: @Composable (() -> Unit)?, borderWidth: Dp, borderColor: Color, indicatorWidth: Dp, indicatorColor: Color, cursorColor: Color, backgroundColor: Color, shape: Shape, ) { BasicTextField( value = value, modifier = Modifier .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight ) .background(backgroundColor, shape) .then(modifier), onValueChange = onValueChange, enabled = enabled, readOnly = readOnly, textStyle = textStyle, cursorBrush = SolidColor(cursorColor), visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, maxLines = maxLines, decorationBox = @Composable { coreTextField -> // places leading icon, input field, label, placeholder, trailing icon IconsWithTextFieldLayout( textField = coreTextField, leading = leading, trailing = trailing, singleLine = singleLine, placeholder = decoratedPlaceholder, shape = shape, borderWidth = borderWidth, borderColor = borderColor, indicatorWidth = indicatorWidth, indicatorColor = indicatorColor, ) } ) } private fun Modifier.textFieldIndicator(color: Color, shape: Shape, width: Dp, cornerRadius: Dp): Modifier { if (color.alpha == 0f) return this return drawBehind { val controlOutline = shape.createOutline(size, layoutDirection, this) val highlightOutline = RoundRect(controlOutline.bounds.inflate(width.toPx()), CornerRadius(cornerRadius.toPx())) val highlightPath = Path().apply { this.fillType = PathFillType.EvenOdd addRoundRect(highlightOutline) addOutline(controlOutline) close() } drawPath(highlightPath, color) } } @Composable private fun IconsWithTextFieldLayout( textField: @Composable () -> Unit, placeholder: @Composable ((Modifier) -> Unit)?, leading: @Composable (() -> Unit)?, trailing: @Composable (() -> Unit)?, singleLine: Boolean, shape: Shape, borderWidth: Dp, borderColor: Color, indicatorWidth: Dp, indicatorColor: Color, ) { Layout( content = { Box( Modifier .layoutId("border") .border( width = borderWidth, color = borderColor, shape = shape ) .textFieldIndicator( width = indicatorWidth, color = indicatorColor, shape = shape, cornerRadius = 4.dp ) ) if (leading != null) { Box( modifier = Modifier.layoutId(LeadingId).then(IconDefaultSizeModifier) .padding(start = HorizontalIconPadding), contentAlignment = Alignment.Center ) { leading() } } if (trailing != null) { Box( modifier = Modifier.layoutId(TrailingId).then(IconDefaultSizeModifier) .padding(end = HorizontalIconPadding), contentAlignment = Alignment.Center ) { trailing() } } val padding = Modifier.padding( start = HorizontalTextFieldPadding, end = HorizontalTextFieldPadding ) if (placeholder != null) { placeholder(Modifier.layoutId(PlaceholderId).then(padding)) } Box( modifier = Modifier.layoutId(TextFieldId).then(padding), propagateMinConstraints = true ) { textField() } } ) { measurables, incomingConstraints -> // used to calculate the constraints for measuring elements that will be placed in a row var occupiedSpaceHorizontally = 0 val bottomPadding = VerticalTextFieldPadding.roundToPx() // measure leading icon val constraints = incomingConstraints.copy(minWidth = 0, minHeight = 0) val leadingPlaceable = measurables.find { it.layoutId == LeadingId }?.measure(constraints) occupiedSpaceHorizontally += widthOrZero( leadingPlaceable ) // measure trailing icon val trailingPlaceable = measurables.find { it.layoutId == TrailingId } ?.measure(constraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += widthOrZero( trailingPlaceable ) // measure label constraints.offset( horizontal = -occupiedSpaceHorizontally, vertical = -bottomPadding ) // measure text field // on top we offset either by default padding or by label's half height if its too big // minWidth must not be set to 0 due to how foundation TextField treats zero minWidth val topPadding = bottomPadding val textConstraints = incomingConstraints.offset( horizontal = -occupiedSpaceHorizontally, vertical = -bottomPadding - topPadding ).copy(minHeight = 0) val textFieldPlaceable = measurables.first { it.layoutId == TextFieldId }.measure(textConstraints) // measure placeholder val placeholderConstraints = textConstraints.copy(minWidth = 0) val placeholderPlaceable = measurables.find { it.layoutId == PlaceholderId }?.measure(placeholderConstraints) val width = calculateWidth( leadingPlaceable, trailingPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints ) val height = calculateHeight( leadingPlaceable, trailingPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints, density ) val borderPlaceable = measurables.first { it.layoutId == "border" }.measure( Constraints( minWidth = if (width != Constraints.Infinity) width else 0, maxWidth = width, minHeight = if (height != Constraints.Infinity) height else 0, maxHeight = height ) ) layout(width, height) { place( height, width, leadingPlaceable, trailingPlaceable, textFieldPlaceable, placeholderPlaceable, borderPlaceable, singleLine, density ) } } } private fun calculateWidth( leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, ): Int { val middleSection = maxOf( textFieldPlaceable.width, widthOrZero(placeholderPlaceable) ) val wrappedWidth = widthOrZero(leadingPlaceable) + middleSection + widthOrZero( trailingPlaceable ) return max(wrappedWidth, constraints.minWidth) } private fun calculateHeight( leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, density: Float, ): Int { // middle section is defined as a height of the text field or placeholder ( whichever is // taller) plus 16.dp or half height of the label if it is taller, given that the label // is vertically centered to the top edge of the resulting text field's container val inputFieldHeight = max( textFieldPlaceable.height, heightOrZero(placeholderPlaceable) ) val topBottomPadding = VerticalTextFieldPadding.value * density val middleSectionHeight = inputFieldHeight + topBottomPadding + topBottomPadding return max( constraints.minHeight, maxOf( heightOrZero(leadingPlaceable), heightOrZero(trailingPlaceable), middleSectionHeight.roundToInt() ) ) } private fun Placeable.PlacementScope.place( height: Int, width: Int, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, borderPlaceable: Placeable, singleLine: Boolean, density: Float, ) { val topBottomPadding = (VerticalTextFieldPadding.value * density).roundToInt() // placed center vertically and to the start edge horizontally leadingPlaceable?.placeRelative( 0, Alignment.CenterVertically.align(leadingPlaceable.height, height) ) // placed center vertically and to the end edge horizontally trailingPlaceable?.placeRelative( width - trailingPlaceable.width, Alignment.CenterVertically.align(trailingPlaceable.height, height) ) // placed center vertically and after the leading icon horizontally if single line text field // placed to the top with padding for multi line text field val textVerticalPosition = if (singleLine) { Alignment.CenterVertically.align(textFieldPlaceable.height, height) } else { topBottomPadding } textFieldPlaceable.placeRelative(widthOrZero(leadingPlaceable), textVerticalPosition) // placed similar to the input text above placeholderPlaceable?.let { val placeholderVerticalPosition = if (singleLine) { Alignment.CenterVertically.align(it.height, height) } else { topBottomPadding } it.placeRelative(widthOrZero(leadingPlaceable), placeholderVerticalPosition) } // place border borderPlaceable.place(IntOffset.Zero) } @Composable internal fun Decoration( contentColor: Color, typography: TextStyle? = null, contentAlpha: Float? = null, content: @Composable () -> Unit, ) { val colorAndEmphasis: @Composable () -> Unit = @Composable { CompositionLocalProvider(LocalContentColor provides contentColor) { if (contentAlpha != null) { CompositionLocalProvider( LocalContentAlpha provides contentAlpha, content = content ) } else { CompositionLocalProvider( LocalContentAlpha provides contentColor.alpha, content = content ) } } } if (typography != null) ProvideTextStyle(typography, colorAndEmphasis) else colorAndEmphasis() } object TextFieldDefaults { val MinHeight = 24.dp val MinWidth = 64.dp @Composable fun textFieldStyle( textColor: Color = JBTheme.textColors.default, disabledTextColor: Color = JBTheme.textColors.disabled, errorTextColor: Color = JBTheme.textColors.error, backgroundColor: Color = JBTheme.fieldColors.bg, disabledBackgroundColor: Color = JBTheme.fieldColors.bgDisabled, placeholderColor: Color = JBTheme.textColors.infoInput, disabledPlaceholderColor: Color = JBTheme.textColors.infoInput, borderColor: Color = JBTheme.fieldColors.border, disabledBorderColor: Color = JBTheme.fieldColors.borderDisabled, errorBorderColor: Color = JBTheme.fieldColors.borderError, focusedBorderColor: Color = JBTheme.fieldColors.borderFocused, indicatorColor: Color = JBTheme.focusColors.default, errorIndicatorColor: Color = JBTheme.focusColors.error, cursorColor: Color = JBTheme.textColors.default, errorCursorColor: Color = JBTheme.textColors.error, ): TextFieldStyle = DefaultTextFieldStyle( textColor, disabledTextColor, errorTextColor, backgroundColor, disabledBackgroundColor, placeholderColor, disabledPlaceholderColor, borderColor, disabledBorderColor, errorBorderColor, focusedBorderColor, indicatorColor, errorIndicatorColor, cursorColor, errorCursorColor ) } internal fun widthOrZero(placeable: Placeable?) = placeable?.width ?: 0 internal fun heightOrZero(placeable: Placeable?) = placeable?.height ?: 0 internal val HorizontalTextFieldPadding = 6.dp internal val VerticalTextFieldPadding = 3.dp internal val HorizontalIconPadding = 6.dp internal val IconDefaultSizeModifier = Modifier.defaultMinSize(16.dp, 16.dp) private const val PlaceholderId = "Placeholder" private const val TextFieldId = "TextField" private const val LeadingId = "Leading" private const val TrailingId = "Trailing" ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/__JBIcons.kt ================================================ package io.kanro.compose.jetbrains.icons internal object JBIcons ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/__Actions.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons import io.kanro.compose.jetbrains.icons.JBIcons internal object ActionsGroup internal val JBIcons.Actions: ActionsGroup get() = ActionsGroup ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/__General.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons import io.kanro.compose.jetbrains.icons.JBIcons internal object GeneralGroup internal val JBIcons.General: GeneralGroup get() = GeneralGroup ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/ArrowExpand.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.actions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup internal val ActionsGroup.ArrowExpand: ImageVector get() { if (_arrowExpand != null) { return _arrowExpand!! } _arrowExpand = Builder( name = "ArrowExpand", defaultWidth = 16.0.dp, defaultHeight = 16.0.dp, viewportWidth = 16.0f, viewportHeight = 16.0f ).apply { path( fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF6E6E6E)), strokeLineWidth = 2.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(6.0f, 13.0f) lineTo(11.0f, 8.0f) lineTo(6.0f, 3.0f) } } .build() return _arrowExpand!! } private var _arrowExpand: ImageVector? = null ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/ArrowExpandDark.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.actions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup internal val ActionsGroup.ArrowExpandDark: ImageVector get() { if (_arrowExpandDark != null) { return _arrowExpandDark!! } _arrowExpandDark = Builder( name = "ArrowexpandDark", defaultWidth = 16.0.dp, defaultHeight = 16.0.dp, viewportWidth = 16.0f, viewportHeight = 16.0f ).apply { path( fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFFAFB1B3)), strokeLineWidth = 2.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(6.0f, 13.0f) lineTo(11.0f, 8.0f) lineTo(6.0f, 3.0f) } } .build() return _arrowExpandDark!! } private var _arrowExpandDark: ImageVector? = null ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/Checkmark.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.actions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup internal val ActionsGroup.Checkmark: ImageVector get() { if (_checkmark != null) { return _checkmark!! } _checkmark = Builder( name = "Checkmark", defaultWidth = 14.0.dp, defaultHeight = 14.0.dp, viewportWidth = 14.0f, viewportHeight = 14.0f ).apply { path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(5.625f, 8.4267f) lineTo(9.5566f, 2.9336f) curveTo(9.5566f, 2.9336f, 10.1737f, 2.3242f, 10.8612f, 2.8242f) curveTo(11.4433f, 3.3867f, 10.998f, 4.0938f, 10.998f, 4.0938f) lineTo(6.3183f, 10.6445f) curveTo(6.3183f, 10.6445f, 5.9941f, 11.0f, 5.5839f, 11.0f) curveTo(5.1737f, 11.0f, 4.873f, 10.6445f, 4.873f, 10.6445f) lineTo(2.9394f, 7.7461f) curveTo(2.9394f, 7.7461f, 2.5683f, 6.9805f, 3.2558f, 6.4609f) curveTo(4.0605f, 6.0781f, 4.5605f, 6.8394f, 4.5605f, 6.8394f) lineTo(5.625f, 8.4267f) close() } } .build() return _checkmark!! } private var _checkmark: ImageVector? = null ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/CheckmarkIndeterminate.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.actions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.NonZero import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup internal val ActionsGroup.CheckmarkIndeterminate: ImageVector get() { if (_checkmarkIndeterminate != null) { return _checkmarkIndeterminate!! } _checkmarkIndeterminate = Builder( name = "CheckmarkIndeterminate", defaultWidth = 14.0.dp, defaultHeight = 14.0.dp, viewportWidth = 14.0f, viewportHeight = 14.0f ).apply { path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = NonZero ) { moveTo(3.7402f, 5.73f) lineTo(10.1402f, 5.73f) arcTo(1.0f, 1.0f, 0.0f, false, true, 11.1402f, 6.73f) lineTo(11.1402f, 7.23f) arcTo(1.0f, 1.0f, 0.0f, false, true, 10.1402f, 8.23f) lineTo(3.7402f, 8.23f) arcTo(1.0f, 1.0f, 0.0f, false, true, 2.7402f, 7.23f) lineTo(2.7402f, 6.73f) arcTo(1.0f, 1.0f, 0.0f, false, true, 3.7402f, 5.73f) close() } } .build() return _checkmarkIndeterminate!! } private var _checkmarkIndeterminate: ImageVector? = null ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/actions/Close.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.actions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.ActionsGroup internal val ActionsGroup.Close: ImageVector get() { if (_close != null) { return _close!! } _close = Builder( name = "Close", defaultWidth = 16.0.dp, defaultHeight = 16.0.dp, viewportWidth = 16.0f, viewportHeight = 16.0f ).apply { path( fill = SolidColor(Color(0xFF7F8B91)), stroke = null, fillAlpha = 0.5f, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(7.9949f, 8.7051f) lineTo(4.8541f, 11.8541f) lineTo(4.147f, 11.147f) lineTo(7.2949f, 8.0051f) lineTo(4.147f, 4.8571f) lineTo(4.8541f, 4.15f) lineTo(8.002f, 7.298f) lineTo(11.144f, 4.15f) lineTo(11.8511f, 4.8571f) lineTo(8.702f, 7.998f) lineTo(11.8511f, 11.147f) lineTo(11.144f, 11.8541f) lineTo(7.9949f, 8.7051f) close() } } .build() return _close!! } private var _close: ImageVector? = null ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/general/ButtonDropTriangle.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.general import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.GeneralGroup internal val GeneralGroup.ButtonDropTriangle: ImageVector get() { if (_buttonDropTriangle != null) { return _buttonDropTriangle!! } _buttonDropTriangle = Builder( name = "ButtonDropTriangle", defaultWidth = 8.0.dp, defaultHeight = 4.0.dp, viewportWidth = 8.0f, viewportHeight = 4.0f ).apply { path( fill = SolidColor(Color(0xFF6E6E6E)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(4.0f, 4.0f) lineToRelative(4.0f, -4.0f) lineToRelative(-8.0f, -0.0f) close() } } .build() return _buttonDropTriangle!! } private var _buttonDropTriangle: ImageVector? = null ================================================ FILE: classic/src/main/kotlin/io/kanro/compose/jetbrains/icons/jbicons/general/ButtonDropTriangleDark.kt ================================================ package io.kanro.compose.jetbrains.icons.jbicons.general import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.icons.jbicons.GeneralGroup internal val GeneralGroup.ButtonDropTriangleDark: ImageVector get() { if (_buttonDropTriangleDark != null) { return _buttonDropTriangleDark!! } _buttonDropTriangleDark = Builder( name = "ButtonDropTriangleDark", defaultWidth = 8.0.dp, defaultHeight = 4.0.dp, viewportWidth = 8.0f, viewportHeight = 4.0f ).apply { path( fill = SolidColor(Color(0xFFAFB1B3)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, pathFillType = EvenOdd ) { moveTo(4.0f, 4.0f) lineToRelative(4.0f, -4.0f) lineToRelative(-8.0f, -0.0f) close() } } .build() return _buttonDropTriangleDark!! } private var _buttonDropTriangleDark: ImageVector? = null ================================================ FILE: expui/build.gradle.kts ================================================ plugins { kotlin("jvm") id("com.netflix.nebula.maven-publish") id("com.netflix.nebula.source-jar") id("com.bybutter.sisyphus.project") id("org.jetbrains.compose") `java-library` } description = "JetBrains ExpUI Kit for Compose Desktop" dependencies { implementation(kotlin("stdlib")) implementation(compose.desktop.common) { exclude("org.jetbrains.compose.material") } } tasks.withType() { kotlinOptions.jvmTarget = "17" } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/DesktopPlatform.kt ================================================ package io.kanro.compose.jetbrains.expui enum class DesktopPlatform { Linux, Windows, MacOS, Unknown; companion object { val Current: DesktopPlatform by lazy { val name = System.getProperty("os.name") when { name?.startsWith("Linux") == true -> Linux name?.startsWith("Win") == true -> Windows name == "Mac OS X" -> MacOS else -> Unknown } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ActionButton.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.Indication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme class ActionButtonColors( override val normalAreaColors: AreaColors, override val hoverAreaColors: AreaColors, override val pressedAreaColors: AreaColors, override val disabledAreaColors: AreaColors, ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, DisabledAreaProvider { @Composable fun provideArea(enabled: Boolean, content: @Composable () -> Unit) { CompositionLocalProvider( LocalAreaColors provides if (enabled) normalAreaColors else disabledAreaColors, LocalNormalAreaColors provides normalAreaColors, LocalDisabledAreaColors provides disabledAreaColors, LocalHoverAreaColors provides hoverAreaColors, LocalPressedAreaColors provides pressedAreaColors, content = content ) } } val LocalActionButtonColors = compositionLocalOf { LightTheme.ActionButtonColors } @Composable fun ActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = RoundedCornerShape(6.dp), indication: Indication? = HoverOrPressedIndication(shape), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: ActionButtonColors = LocalActionButtonColors.current, content: @Composable BoxScope.() -> Unit, ) { colors.provideArea(enabled) { Box( modifier.areaBackground(shape = shape).clickable( interactionSource = interactionSource, indication = indication, enabled = enabled, onClick = onClick, role = Role.Button ), propagateMinConstraints = true ) { content() } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Button.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.theme.LightTheme class ButtonColors( override val normalAreaColors: AreaColors, override val focusAreaColors: AreaColors, override val disabledAreaColors: AreaColors, ) : AreaProvider, FocusAreaProvider, DisabledAreaProvider { @Composable fun provideArea(enabled: Boolean, focused: Boolean, content: @Composable () -> Unit) { val currentAreaColor = when { !enabled -> disabledAreaColors focused -> focusAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentAreaColor, LocalNormalAreaColors provides normalAreaColors, LocalFocusAreaColors provides focusAreaColors, LocalDisabledAreaColors provides disabledAreaColors, content = content ) } } val LocalPrimaryButtonColors = compositionLocalOf { LightTheme.PrimaryButtonColors } val LocalOutlineButtonColors = compositionLocalOf { LightTheme.OutlineButtonColors } @Composable fun OutlineButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: ButtonColors = LocalOutlineButtonColors.current, content: @Composable RowScope.() -> Unit, ) { ButtonImpl(onClick, modifier, enabled, interactionSource, colors, content) } @Composable fun PrimaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: ButtonColors = LocalPrimaryButtonColors.current, content: @Composable RowScope.() -> Unit, ) { ButtonImpl(onClick, modifier, enabled, interactionSource, colors, content) } @Composable private fun ButtonImpl( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: ButtonColors, content: @Composable RowScope.() -> Unit, ) { val isFocused = remember { mutableStateOf(false) } colors.provideArea(enabled, isFocused.value) { val areaColors = LocalAreaColors.current Box( Modifier.defaultMinSize(72.dp, 24.dp).drawWithCache { onDrawBehind { if (isFocused.value) { drawRoundRect( areaColors.focusColor, size = Size(size.width + 4.dp.toPx(), size.height + 4.dp.toPx()), topLeft = Offset(-2.dp.toPx(), -2.dp.toPx()), cornerRadius = CornerRadius(5.dp.toPx()) ) } drawRoundRect(areaColors.startBorderColor, cornerRadius = CornerRadius(3.dp.toPx())) drawRoundRect( areaColors.startBackground, size = Size(size.width - 2.dp.toPx(), size.height - 2.dp.toPx()), topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), cornerRadius = CornerRadius(2.dp.toPx()) ) } }.onFocusEvent { isFocused.value = it.isFocused }.clickable( interactionSource = interactionSource, indication = null, enabled = enabled, onClick = onClick, role = Role.Button ).then(modifier), contentAlignment = Alignment.Center ) { Row( modifier = Modifier.padding(14.dp, 3.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { content() } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/CheckBox.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.Canvas import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.triStateToggleable import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.style.SelectionAreaProvider import io.kanro.compose.jetbrains.expui.theme.LightTheme class CheckBoxColors( override val normalAreaColors: AreaColors, override val selectionAreaColors: AreaColors, override val focusAreaColors: AreaColors, override val disabledAreaColors: AreaColors, ) : AreaProvider, DisabledAreaProvider, FocusAreaProvider, SelectionAreaProvider { @Composable fun provideArea(enabled: Boolean, focused: Boolean, selected: Boolean, content: @Composable () -> Unit) { val currentColors = when { !enabled -> disabledAreaColors focused -> focusAreaColors selected -> selectionAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides normalAreaColors, LocalSelectionAreaColors provides selectionAreaColors, LocalFocusAreaColors provides focusAreaColors, LocalDisabledAreaColors provides disabledAreaColors, content = content ) } } val LocalCheckBoxColors = compositionLocalOf { LightTheme.CheckBoxColors } @Composable fun Checkbox( checked: Boolean, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: CheckBoxColors = LocalCheckBoxColors.current, ) { TriStateCheckbox( state = ToggleableState(checked), onClick = { onCheckedChange(!checked) }, interactionSource = interactionSource, enabled = enabled, modifier = modifier, colors = colors ) } @Composable fun Checkbox( checked: Boolean, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: CheckBoxColors = LocalCheckBoxColors.current, content: @Composable () -> Unit, ) { TriStateCheckbox( state = ToggleableState(checked), onClick = { onCheckedChange(!checked) }, interactionSource = interactionSource, enabled = enabled, modifier = modifier, colors = colors, content = content ) } @Composable fun TriStateCheckbox( state: ToggleableState, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: CheckBoxColors = LocalCheckBoxColors.current, ) { val isFocused = remember { mutableStateOf(false) } colors.provideArea(enabled, isFocused.value, state != ToggleableState.Off) { CheckboxImpl( isFocused = isFocused.value, value = state, modifier = Modifier.onFocusEvent { isFocused.value = it.isFocused }.triStateToggleable( state = state, onClick = onClick, enabled = enabled, role = Role.Checkbox, interactionSource = interactionSource, indication = null ) ) } } @Composable fun TriStateCheckbox( state: ToggleableState, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: CheckBoxColors = LocalCheckBoxColors.current, content: @Composable () -> Unit, ) { val isFocused = remember { mutableStateOf(false) } colors.provideArea(enabled, isFocused.value, state != ToggleableState.Off) { Row( modifier.onFocusEvent { isFocused.value = it.isFocused }.triStateToggleable( state = state, onClick = onClick, enabled = enabled, role = Role.Checkbox, interactionSource = interactionSource, indication = null ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { CheckboxImpl(isFocused = isFocused.value, value = state) content() } } } private fun Checkmark() = ImageVector.Builder( name = "Checkmark", defaultWidth = 14.0.dp, defaultHeight = 14.0.dp, viewportWidth = 14.0f, viewportHeight = 14.0f ).apply { path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = StrokeCap.Butt, strokeLineJoin = StrokeJoin.Miter, strokeLineMiter = 4.0f, pathFillType = PathFillType.EvenOdd ) { moveTo(5.625f, 8.4267f) lineTo(9.5566f, 2.9336f) curveTo(9.5566f, 2.9336f, 10.1737f, 2.3242f, 10.8612f, 2.8242f) curveTo(11.4433f, 3.3867f, 10.998f, 4.0938f, 10.998f, 4.0938f) lineTo(6.3183f, 10.6445f) curveTo(6.3183f, 10.6445f, 5.9941f, 11.0f, 5.5839f, 11.0f) curveTo(5.1737f, 11.0f, 4.873f, 10.6445f, 4.873f, 10.6445f) lineTo(2.9394f, 7.7461f) curveTo(2.9394f, 7.7461f, 2.5683f, 6.9805f, 3.2558f, 6.4609f) curveTo(4.0605f, 6.0781f, 4.5605f, 6.8394f, 4.5605f, 6.8394f) lineTo(5.625f, 8.4267f) close() } }.build() private fun CheckmarkIndeterminate() = ImageVector.Builder( name = "CheckmarkIndeterminate", defaultWidth = 14.0.dp, defaultHeight = 14.0.dp, viewportWidth = 14.0f, viewportHeight = 14.0f ).apply { path( fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f, strokeLineCap = StrokeCap.Butt, strokeLineJoin = StrokeJoin.Miter, strokeLineMiter = 4.0f, pathFillType = PathFillType.NonZero ) { moveTo(3.7402f, 5.73f) lineTo(10.1402f, 5.73f) arcTo(1.0f, 1.0f, 0.0f, false, true, 11.1402f, 6.73f) lineTo(11.1402f, 7.23f) arcTo(1.0f, 1.0f, 0.0f, false, true, 10.1402f, 8.23f) lineTo(3.7402f, 8.23f) arcTo(1.0f, 1.0f, 0.0f, false, true, 2.7402f, 7.23f) lineTo(2.7402f, 6.73f) arcTo(1.0f, 1.0f, 0.0f, false, true, 3.7402f, 5.73f) close() } }.build() @Composable private fun CheckboxImpl( isFocused: Boolean, value: ToggleableState, modifier: Modifier = Modifier, ) { val icon = when (value) { ToggleableState.On -> rememberVectorPainter(Checkmark()) ToggleableState.Indeterminate -> rememberVectorPainter(CheckmarkIndeterminate()) else -> null } val colors = LocalAreaColors.current Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(14.dp)) { if (isFocused) { drawRoundRect( colors.focusColor, size = Size(18.dp.toPx(), 18.dp.toPx()), topLeft = Offset(-2.dp.toPx(), -2.dp.toPx()), cornerRadius = CornerRadius(4.dp.toPx()) ) } drawRoundRect(colors.startBorderColor, cornerRadius = CornerRadius(2.dp.toPx())) drawRoundRect( colors.startBackground, size = Size(12.dp.toPx(), 12.dp.toPx()), topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), cornerRadius = CornerRadius(1.dp.toPx()) ) if (icon != null) { with(icon) { 14.dp.toPx() draw(Size(14.dp.toPx(), 14.dp.toPx()), colorFilter = ColorFilter.tint(colors.foreground)) } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ComboBox.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors class ComboBoxColors( override val normalAreaColors: AreaColors, override val focusAreaColors: AreaColors, override val disabledAreaColors: AreaColors, val dropdownMenuColors: DropdownMenuColors, ) : AreaProvider, FocusAreaProvider, DisabledAreaProvider { @Composable fun provideArea(enabled: Boolean, focused: Boolean, content: @Composable () -> Unit) { val currentAreaColor = when { !enabled -> disabledAreaColors focused -> focusAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentAreaColor, LocalNormalAreaColors provides normalAreaColors, LocalFocusAreaColors provides focusAreaColors, LocalDisabledAreaColors provides disabledAreaColors, LocalDropdownMenuColors provides dropdownMenuColors, content = content ) } } val LocalComboBoxColors = compositionLocalOf { error("No ComboBoxColors provided") } @Composable fun ComboBox( items: List, value: T, onValueChange: ((T) -> Unit)? = null, modifier: Modifier = Modifier, menuModifier: Modifier = Modifier, enabled: Boolean = true, valueRender: @Composable (T) -> Unit = { Label("$it") }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: ComboBoxColors = LocalComboBoxColors.current, ) { val isFocused = remember { mutableStateOf(false) } var menuOpened by remember { mutableStateOf(false) } colors.provideArea(enabled, isFocused.value) { val areaColors = LocalAreaColors.current Box { Box( Modifier.defaultMinSize(72.dp, 24.dp).drawWithCache { onDrawBehind { if (isFocused.value) { drawRoundRect( areaColors.focusColor, size = Size(size.width + 4.dp.toPx(), size.height + 4.dp.toPx()), topLeft = Offset(-2.dp.toPx(), -2.dp.toPx()), cornerRadius = CornerRadius(5.dp.toPx()) ) } drawRoundRect(areaColors.startBorderColor, cornerRadius = CornerRadius(3.dp.toPx())) drawRoundRect( areaColors.startBackground, size = Size(size.width - 2.dp.toPx(), size.height - 2.dp.toPx()), topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), cornerRadius = CornerRadius(2.dp.toPx()) ) } }.onFocusEvent { isFocused.value = it.isFocused }.clickable( interactionSource = interactionSource, indication = null, enabled = enabled, onClick = { menuOpened = true }, role = Role.Button ).padding(6.dp, 3.dp).then(modifier), contentAlignment = Alignment.CenterStart ) { Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { valueRender(value) } Icon("icons/buttonDropTriangle.svg", modifier = Modifier.align(Alignment.CenterEnd)) } DropdownMenu(menuOpened, { menuOpened = false }, modifier = menuModifier) { items.forEach { item -> val focusRequester = remember { FocusRequester() } DropdownMenuItem( onClick = { if (value != item) { onValueChange?.invoke(item) } menuOpened = false }, Modifier.focusRequester(focusRequester).onFocusEvent { if (it.isFocused && value != item) { onValueChange?.invoke(item) } } ) { valueRender(item) } LaunchedEffect(menuOpened) { if (menuOpened && value == item) { focusRequester.requestFocus() } } } } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ContextCompositionLocals.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.runtime.compositionLocalOf val LocalContentActivated = compositionLocalOf { true } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ContextMenu.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuRepresentation import androidx.compose.foundation.ContextMenuState import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.InputModeManager import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.nativeKeyCode import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalInputModeManager import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.rememberCursorPositionProvider import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.style.areaBorder import io.kanro.compose.jetbrains.expui.theme.LightTheme import java.awt.event.KeyEvent class ContextMenuColors( override val normalAreaColors: AreaColors, override val hoverAreaColors: AreaColors, override val pressedAreaColors: AreaColors, override val focusAreaColors: AreaColors, ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, FocusAreaProvider { @Composable fun provideArea(content: @Composable () -> Unit) { CompositionLocalProvider( LocalNormalAreaColors provides normalAreaColors, LocalAreaColors provides normalAreaColors, LocalHoverAreaColors provides hoverAreaColors, LocalPressedAreaColors provides pressedAreaColors, LocalFocusAreaColors provides focusAreaColors, content = content ) } } val LocalContextMenuColors = compositionLocalOf { LightTheme.ContextMenuColors } class JbContextMenuRepresentation(private val colors: ContextMenuColors) : ContextMenuRepresentation { @Composable override fun Representation(state: ContextMenuState, items: () -> List) { val isOpen = state.status is ContextMenuState.Status.Open ContextMenu( isOpen = isOpen, items = items, onDismissRequest = { state.status = ContextMenuState.Status.Closed }, colors = colors, ) } } @OptIn(ExperimentalComposeUiApi::class) @Composable fun ContextMenu( isOpen: Boolean, items: () -> List, onDismissRequest: () -> Unit, colors: ContextMenuColors = LocalContextMenuColors.current, ) { if (isOpen) { var focusManager: FocusManager? by mutableStateOf(null) var inputModeManager: InputModeManager? by mutableStateOf(null) Popup( focusable = true, onDismissRequest = onDismissRequest, popupPositionProvider = rememberCursorPositionProvider(), onKeyEvent = { if (it.type == KeyEventType.KeyDown) { when (it.key.nativeKeyCode) { KeyEvent.VK_ESCAPE -> { onDismissRequest() true } KeyEvent.VK_DOWN -> { inputModeManager!!.requestInputMode(InputMode.Keyboard) focusManager!!.moveFocus(FocusDirection.Next) true } KeyEvent.VK_UP -> { inputModeManager!!.requestInputMode(InputMode.Keyboard) focusManager!!.moveFocus(FocusDirection.Previous) true } else -> false } } else { false } }, ) { focusManager = LocalFocusManager.current inputModeManager = LocalInputModeManager.current colors.provideArea { Box( modifier = Modifier.shadow(12.dp).areaBorder().areaBackground() .sizeIn(maxHeight = 600.dp, minWidth = 72.dp).padding(6.dp).width(IntrinsicSize.Max) ) { val scrollState = rememberScrollState() Column( modifier = Modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(4.dp) ) { items().forEach { item -> MenuItemContent(onClick = { onDismissRequest() item.onClick() }) { Label(text = item.label) } } } Box(modifier = Modifier.matchParentSize()) { VerticalScrollbar( rememberScrollbarAdapter(scrollState), modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd) ) } } } } } } @Composable private fun MenuItemContent( onClick: () -> Unit, contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp), shape: Shape = RoundedCornerShape(3.dp), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable RowScope.() -> Unit, ) { val focused = remember { mutableStateOf(false) } val focusedColors = LocalFocusAreaColors.current Row( modifier = Modifier.drawWithCache { onDrawBehind { if (focused.value) { val outline = shape.createOutline(size, layoutDirection, this) drawOutline(outline, focusedColors.startBackground) } } }.onFocusEvent { focused.value = it.isFocused }.clickable( enabled = true, onClick = onClick, interactionSource = interactionSource, indication = HoverOrPressedIndication(shape) ).fillMaxWidth().padding(contentPadding).defaultMinSize(minHeight = 24.dp), verticalAlignment = Alignment.CenterVertically ) { content() } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/DropdownMenu.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.InputModeManager import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalInputModeManager import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.rememberCursorPositionProvider import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.style.areaBorder import io.kanro.compose.jetbrains.expui.theme.LightTheme import java.awt.event.KeyEvent import kotlin.math.max import kotlin.math.min class DropdownMenuColors( override val normalAreaColors: AreaColors, override val hoverAreaColors: AreaColors, override val pressedAreaColors: AreaColors, override val focusAreaColors: AreaColors, ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, FocusAreaProvider { @Composable fun provideArea(content: @Composable () -> Unit) { CompositionLocalProvider( LocalNormalAreaColors provides normalAreaColors, LocalAreaColors provides normalAreaColors, LocalHoverAreaColors provides hoverAreaColors, LocalPressedAreaColors provides pressedAreaColors, LocalFocusAreaColors provides focusAreaColors, content = content ) } } val LocalDropdownMenuColors = compositionLocalOf { LightTheme.DropdownMenuColors } @Composable fun DropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, focusable: Boolean = true, modifier: Modifier = Modifier, offset: DpOffset = DpOffset(0.dp, 0.dp), colors: DropdownMenuColors = LocalDropdownMenuColors.current, content: @Composable ColumnScope.() -> Unit, ) { val expandedStates = remember { MutableTransitionState(false) } expandedStates.targetState = expanded if (expandedStates.currentState || expandedStates.targetState) { val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } val density = LocalDensity.current // The original [DropdownMenuPositionProvider] is not yet suitable for large screen devices, // so we need to make additional checks and adjust the position of the [DropdownMenu] to // avoid content being cut off if the [DropdownMenu] contains too many items. // See: https://github.com/JetBrains/compose-jb/issues/1388 val popupPositionProvider = DesktopDropdownMenuPositionProvider( offset, density ) { parentBounds, menuBounds -> transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds) } var focusManager: FocusManager? by mutableStateOf(null) var inputModeManager: InputModeManager? by mutableStateOf(null) Popup( focusable = focusable, onDismissRequest = onDismissRequest, popupPositionProvider = popupPositionProvider, onKeyEvent = { handlePopupOnKeyEvent(it, onDismissRequest, focusManager!!, inputModeManager!!) }, ) { focusManager = LocalFocusManager.current inputModeManager = LocalInputModeManager.current DropdownMenuContent( modifier = modifier, colors = colors, content = content ) } } } @Composable fun DropdownMenuItem( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = RectangleShape, contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable RowScope.() -> Unit, ) { DropdownMenuItemContent( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, contentPadding = contentPadding, interactionSource = interactionSource, content = content ) } @OptIn(ExperimentalComposeUiApi::class) private fun handlePopupOnKeyEvent( keyEvent: androidx.compose.ui.input.key.KeyEvent, onDismissRequest: () -> Unit, focusManager: FocusManager, inputModeManager: InputModeManager, ): Boolean { return if (keyEvent.type == KeyEventType.KeyDown && keyEvent.awtEventOrNull?.keyCode == KeyEvent.VK_ESCAPE) { onDismissRequest() true } else if (keyEvent.type == KeyEventType.KeyDown) { when (keyEvent.key) { Key.DirectionDown -> { inputModeManager.requestInputMode(InputMode.Keyboard) focusManager.moveFocus(FocusDirection.Next) true } Key.DirectionUp -> { inputModeManager.requestInputMode(InputMode.Keyboard) focusManager.moveFocus(FocusDirection.Previous) true } else -> false } } else { false } } @Composable fun CursorDropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, focusable: Boolean = true, modifier: Modifier = Modifier, colors: DropdownMenuColors = LocalDropdownMenuColors.current, content: @Composable ColumnScope.() -> Unit, ) { val expandedStates = remember { MutableTransitionState(false) } expandedStates.targetState = expanded if (expandedStates.currentState || expandedStates.targetState) { var focusManager: FocusManager? by mutableStateOf(null) var inputModeManager: InputModeManager? by mutableStateOf(null) Popup( focusable = focusable, onDismissRequest = onDismissRequest, popupPositionProvider = rememberCursorPositionProvider(), onKeyEvent = { handlePopupOnKeyEvent(it, onDismissRequest, focusManager!!, inputModeManager!!) }, ) { focusManager = LocalFocusManager.current inputModeManager = LocalInputModeManager.current DropdownMenuContent( modifier = modifier, colors = colors, content = content ) } } } @Immutable internal data class DesktopDropdownMenuPositionProvider( val contentOffset: DpOffset, val density: Density, val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }, ) : PopupPositionProvider { override fun calculatePosition( anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize, ): IntOffset { // The min margin above and below the menu, relative to the screen. val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() } // The content offset specified using the dropdown offset parameter. val contentOffsetX = with(density) { contentOffset.x.roundToPx() } val contentOffsetY = with(density) { contentOffset.y.roundToPx() } // Compute horizontal position. val toRight = anchorBounds.left + contentOffsetX val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width val toDisplayRight = windowSize.width - popupContentSize.width val toDisplayLeft = 0 val x = if (layoutDirection == LayoutDirection.Ltr) { sequenceOf(toRight, toLeft, toDisplayRight) } else { sequenceOf(toLeft, toRight, toDisplayLeft) }.firstOrNull { it >= 0 && it + popupContentSize.width <= windowSize.width } ?: toLeft // Compute vertical position. val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height val toCenter = anchorBounds.top - popupContentSize.height / 2 val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin var y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { it >= verticalMargin && it + popupContentSize.height <= windowSize.height - verticalMargin } ?: toTop // Desktop specific vertical position checking val aboveAnchor = anchorBounds.top + contentOffsetY val belowAnchor = windowSize.height - anchorBounds.bottom - contentOffsetY if (belowAnchor >= aboveAnchor) { y = anchorBounds.bottom + contentOffsetY } if (y + popupContentSize.height > windowSize.height) { y = windowSize.height - popupContentSize.height } y = y.coerceAtLeast(0) onPositionCalculated( anchorBounds, IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height) ) return IntOffset(x, y) } } @Composable internal fun DropdownMenuContent( modifier: Modifier = Modifier, colors: DropdownMenuColors = LocalDropdownMenuColors.current, content: @Composable ColumnScope.() -> Unit, ) { colors.provideArea { val scrollState = rememberScrollState() Box( modifier = modifier.shadow(12.dp) .areaBorder() .areaBackground() .sizeIn(maxHeight = 600.dp, minWidth = 72.dp) .width(IntrinsicSize.Max) ) { Column( modifier = Modifier.verticalScroll(scrollState), content = content ) Box(modifier = Modifier.matchParentSize()) { VerticalScrollbar( rememberScrollbarAdapter(scrollState), modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd) ) } } } } @Composable internal fun DropdownMenuItemContent( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = RectangleShape, contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable RowScope.() -> Unit, ) { val focused = remember { mutableStateOf(false) } val focusedColors = LocalFocusAreaColors.current Row( modifier = modifier.drawWithCache { onDrawBehind { if (focused.value) { val outline = shape.createOutline(size, layoutDirection, this) drawOutline(outline, focusedColors.startBackground) } } }.onFocusEvent { focused.value = it.isFocused }.clickable( enabled = enabled, onClick = onClick, interactionSource = interactionSource, indication = HoverOrPressedIndication(shape) ).fillMaxWidth().padding(contentPadding).defaultMinSize(minHeight = 24.dp), verticalAlignment = Alignment.CenterVertically ) { content() } } internal fun calculateTransformOrigin( parentBounds: IntRect, menuBounds: IntRect, ): TransformOrigin { val pivotX = when { menuBounds.left >= parentBounds.right -> 0f menuBounds.right <= parentBounds.left -> 1f menuBounds.width == 0 -> 0f else -> { val intersectionCenter = (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2 (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width } } val pivotY = when { menuBounds.top >= parentBounds.bottom -> 0f menuBounds.bottom <= parentBounds.top -> 1f menuBounds.height == 0 -> 0f else -> { val intersectionCenter = (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2 (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height } } return TransformOrigin(pivotX, pivotY) } internal val MenuVerticalMargin = 12.dp ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Icon.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toolingGraphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.theme.LocalIsDarkTheme @Composable fun themedSvgResource(resource: String, isDark: Boolean = LocalIsDarkTheme.current): Painter { var realResource = resource if (isDark) { if (!realResource.endsWith("_dark.svg")) { val dark = realResource.replace(".svg", "_dark.svg") if (Thread.currentThread().contextClassLoader.getResource(dark) != null) { realResource = dark } } } else { if (realResource.endsWith("_dark.svg")) { val light = realResource.replace("_dark.svg", ".svg") if (Thread.currentThread().contextClassLoader.getResource(light) != null) { realResource = light } } } return painterResource(realResource) } private fun Modifier.defaultSizeFor(painter: Painter) = this.then( if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) { DefaultIconSizeModifier } else { Modifier } ) private fun Size.isInfinite() = width.isInfinite() && height.isInfinite() private val DefaultIconSizeModifier = Modifier.size(20.dp) @Composable fun Icon( resource: String, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, markerColor: Color = Color.Unspecified, ) { Icon( themedSvgResource(resource), contentDescription, modifier, colorFilter = colorFilter, markerColor = markerColor, ) } @Composable fun Icon( bitmap: ImageBitmap, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, markerColor: Color = Color.Unspecified, ) { val painter = remember(bitmap) { BitmapPainter(bitmap) } Icon( painter = painter, contentDescription = contentDescription, modifier = modifier, colorFilter = colorFilter, markerColor = markerColor, ) } @Composable fun Icon( imageVector: ImageVector, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, markerColor: Color = Color.Unspecified, ) { Icon( painter = rememberVectorPainter(imageVector), contentDescription = contentDescription, modifier = modifier, colorFilter = colorFilter, markerColor = markerColor, ) } @Composable fun Icon( painter: Painter, contentDescription: String? = null, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, markerColor: Color = Color.Unspecified, ) { val semantics = if (contentDescription != null) { Modifier.semantics { this.contentDescription = contentDescription this.role = Role.Image } } else { Modifier } val filter = colorFilter ?: run { val foreground = LocalAreaColors.current.foreground if (foreground.isSpecified) { ColorFilter.tint(foreground) } else { null } } Box( modifier.toolingGraphicsLayer() .defaultSizeFor(painter) .paintWithMarker(painter, contentScale = ContentScale.None, colorFilter = filter, markerColor = markerColor) .then(semantics) ) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Indication.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.Indication import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.isSpecified import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors class HoverOrPressedIndication(private val shape: Shape) : Indication { private class IndicationInstance( private val shape: Shape, private val isHover: State, private val isPressed: State, private val hoverColor: Color, private val pressedColor: Color, ) : androidx.compose.foundation.IndicationInstance { override fun ContentDrawScope.drawIndication() { when { isPressed.value -> { if (pressedColor.isSpecified) { val outline = shape.createOutline(size, layoutDirection, this) drawOutline(outline, pressedColor) } } isHover.value -> { if (hoverColor.isSpecified) { val outline = shape.createOutline(size, layoutDirection, this) drawOutline(outline, hoverColor) } } } drawContent() } } @Composable override fun rememberUpdatedInstance(interactionSource: InteractionSource): androidx.compose.foundation.IndicationInstance { val hoverColors = LocalHoverAreaColors.current val pressedColors = LocalPressedAreaColors.current val isPressed = interactionSource.collectIsPressedAsState() val isHover = interactionSource.collectIsHoveredAsState() return remember(hoverColors, pressedColors, interactionSource) { IndicationInstance( shape, isHover, isPressed, hoverColors.startBackground, pressedColors.startBackground ) } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Label.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle @Composable fun Label( text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalDefaultTextStyle.current, ) { val textColor = color.takeOrElse { style.color }.takeOrElse { LocalAreaColors.current.text } val mergedStyle = style.merge( TextStyle( color = textColor, fontSize = fontSize, fontWeight = fontWeight, textAlign = textAlign, lineHeight = lineHeight, fontFamily = fontFamily, textDecoration = textDecoration, fontStyle = fontStyle, letterSpacing = letterSpacing ) ) BasicText( text = text, modifier = modifier, style = mergedStyle, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, ) } @Composable fun Label( text: AnnotatedString, modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalDefaultTextStyle.current, ) { val textColor = color.takeOrElse { style.color }.takeOrElse { LocalAreaColors.current.text } val mergedStyle = style.merge( TextStyle( color = textColor, fontSize = fontSize, fontWeight = fontWeight, textAlign = textAlign, lineHeight = lineHeight, fontFamily = fontFamily, textDecoration = textDecoration, fontStyle = fontStyle, letterSpacing = letterSpacing ) ) BasicText( text = text, modifier = modifier, style = mergedStyle, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, inlineContent = inlineContent ) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Link.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.Indication import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider import io.kanro.compose.jetbrains.expui.theme.LightTheme import java.awt.Cursor class LinkColors( override val normalAreaColors: AreaColors, val visitedAreaColors: AreaColors, override val focusAreaColors: AreaColors, override val disabledAreaColors: AreaColors, override val hoverAreaColors: AreaColors, override val pressedAreaColors: AreaColors, ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, DisabledAreaProvider, FocusAreaProvider { @Composable fun provideArea( enabled: Boolean, visited: Boolean, hover: Boolean, pressed: Boolean, content: @Composable () -> Unit, ) { val currentColors = when { !enabled -> disabledAreaColors pressed -> pressedAreaColors hover -> hoverAreaColors visited -> visitedAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides normalAreaColors, LocalDisabledAreaColors provides disabledAreaColors, LocalPressedAreaColors provides pressedAreaColors, LocalHoverAreaColors provides hoverAreaColors, LocalFocusAreaColors provides focusAreaColors, content = content ) } } val LocalLinkColors = compositionLocalOf { LightTheme.LinkColors } @OptIn(ExperimentalComposeUiApi::class) @Composable fun Link( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, indication: Indication? = null, colors: LinkColors = LocalLinkColors.current, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalDefaultTextStyle.current, trailerIcon: @Composable (() -> Unit)? = null, ) { val visited = remember { mutableStateOf(false) } val hovered = remember { mutableStateOf(false) } val pressed = remember { mutableStateOf(false) } colors.provideArea(enabled, visited.value, hovered.value, pressed.value) { val currentAreaColors = LocalAreaColors.current val mergedStyle = style.merge( TextStyle( color = currentAreaColors.text, fontSize = fontSize, fontWeight = fontWeight, textAlign = textAlign, lineHeight = lineHeight, fontFamily = fontFamily, textDecoration = if (hovered.value) { TextDecoration.Underline } else { TextDecoration.None }, fontStyle = fontStyle, letterSpacing = letterSpacing ) ) val focus = remember { mutableStateOf(false) } Box( modifier = Modifier.onFocusEvent { focus.value = it.isFocused }.focusable(enabled, interactionSource).drawWithCache { onDrawBehind { if (focus.value) { val controlOutline = RoundedCornerShape(2.dp).createOutline(size, layoutDirection, this) val highlightOutline = RoundRect(controlOutline.bounds.inflate(2.dp.toPx()), CornerRadius(4.dp.toPx())) val highlightPath = Path().apply { this.fillType = PathFillType.EvenOdd addRoundRect(highlightOutline) addOutline(controlOutline) close() } drawPath(highlightPath, currentAreaColors.focusColor) } } }.onKeyEvent { if (it.type != KeyEventType.KeyUp) return@onKeyEvent false if (!focus.value) return@onKeyEvent false when (it.key) { Key.Enter, Key.NumPadEnter -> { visited.value = true onClick() return@onKeyEvent true } } false } ) { val rowInteractionSource = remember { MutableInteractionSource() } val focusManager = LocalFocusManager.current Row( modifier = modifier.focusProperties { this.canFocus = false }.clickable( onClick = { visited.value = true if (!focus.value) { focusManager.clearFocus() } onClick() }, enabled = enabled, role = Role.Button, indication = indication, interactionSource = rowInteractionSource, ).pointerInput(Unit) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent(PointerEventPass.Initial) if (!enabled) { return@awaitPointerEventScope } when (event.type) { PointerEventType.Enter -> hovered.value = true PointerEventType.Exit -> hovered.value = false PointerEventType.Press -> pressed.value = true PointerEventType.Release -> pressed.value = false else -> {} } } } }.composed { if (enabled) { pointerHoverIcon(PointerIcon(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))) } else { this } } ) { BasicText( text = text, style = mergedStyle, onTextLayout = onTextLayout, overflow = TextOverflow.Clip, softWrap = false, maxLines = 1, ) trailerIcon?.invoke() } } } } @Composable fun ExternalLink( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, indication: Indication? = null, colors: LinkColors = LocalLinkColors.current, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalDefaultTextStyle.current, ) { Link( text = text, onClick = onClick, modifier = modifier, enabled = enabled, fontSize = fontSize, fontStyle = fontStyle, fontWeight = fontWeight, fontFamily = fontFamily, letterSpacing = letterSpacing, textAlign = textAlign, lineHeight = lineHeight, interactionSource = interactionSource, indication = indication, colors = colors, onTextLayout = onTextLayout, style = style, ) { Icon("icons/external_link_arrow.svg") } } @Composable fun DropdownLink( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, indication: Indication? = null, colors: LinkColors = LocalLinkColors.current, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalDefaultTextStyle.current, ) { Link( text = text, onClick = onClick, modifier = modifier, enabled = enabled, fontSize = fontSize, fontStyle = fontStyle, fontWeight = fontWeight, fontFamily = fontFamily, letterSpacing = letterSpacing, textAlign = textAlign, lineHeight = lineHeight, interactionSource = interactionSource, indication = indication, colors = colors, onTextLayout = onTextLayout, style = style, ) { Icon("icons/linkDropTriangle.svg") } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Painter.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.DrawModifier import androidx.compose.ui.draw.paint import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.withSaveLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope import androidx.compose.ui.layout.LayoutModifier import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.times import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.InspectorValueInfo import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import kotlin.math.max import kotlin.math.roundToInt fun Modifier.paintWithMarker( painter: Painter, sizeToIntrinsics: Boolean = true, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Inside, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null, markerColor: Color = Color.Unspecified, ): Modifier { return if (markerColor.isSpecified) { this.then( PainterWithMarkerModifier( painter = painter, sizeToIntrinsics = sizeToIntrinsics, alignment = alignment, contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, markerColor = markerColor, inspectorInfo = debugInspectorInfo { name = "paintWithMarker" properties["painter"] = painter properties["sizeToIntrinsics"] = sizeToIntrinsics properties["alignment"] = alignment properties["contentScale"] = contentScale properties["alpha"] = alpha properties["colorFilter"] = colorFilter properties["markerColor"] = markerColor } ) ) } else { this.paint(painter, sizeToIntrinsics, alignment, contentScale, alpha, colorFilter) } } private class PainterWithMarkerModifier( val painter: Painter, val sizeToIntrinsics: Boolean, val alignment: Alignment = Alignment.Center, val contentScale: ContentScale = ContentScale.Inside, val alpha: Float = DefaultAlpha, val colorFilter: ColorFilter? = null, val markerColor: Color = Color.Unspecified, inspectorInfo: InspectorInfo.() -> Unit, ) : LayoutModifier, DrawModifier, InspectorValueInfo(inspectorInfo) { /** * Helper property to determine if we should size content to the intrinsic * size of the Painter or not. This is only done if [sizeToIntrinsics] is true * and the Painter has an intrinsic size */ private val useIntrinsicSize: Boolean get() = sizeToIntrinsics && painter.intrinsicSize.isSpecified override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { val placeable = measurable.measure(modifyConstraints(constraints)) return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } override fun IntrinsicMeasureScope.minIntrinsicWidth( measurable: IntrinsicMeasurable, height: Int, ): Int { return if (useIntrinsicSize) { val constraints = modifyConstraints(Constraints(maxHeight = height)) val layoutWidth = measurable.minIntrinsicWidth(height) max(constraints.minWidth, layoutWidth) } else { measurable.minIntrinsicWidth(height) } } override fun IntrinsicMeasureScope.maxIntrinsicWidth( measurable: IntrinsicMeasurable, height: Int, ): Int { return if (useIntrinsicSize) { val constraints = modifyConstraints(Constraints(maxHeight = height)) val layoutWidth = measurable.maxIntrinsicWidth(height) max(constraints.minWidth, layoutWidth) } else { measurable.maxIntrinsicWidth(height) } } override fun IntrinsicMeasureScope.minIntrinsicHeight( measurable: IntrinsicMeasurable, width: Int, ): Int { return if (useIntrinsicSize) { val constraints = modifyConstraints(Constraints(maxWidth = width)) val layoutHeight = measurable.minIntrinsicHeight(width) max(constraints.minHeight, layoutHeight) } else { measurable.minIntrinsicHeight(width) } } override fun IntrinsicMeasureScope.maxIntrinsicHeight( measurable: IntrinsicMeasurable, width: Int, ): Int { return if (useIntrinsicSize) { val constraints = modifyConstraints(Constraints(maxWidth = width)) val layoutHeight = measurable.maxIntrinsicHeight(width) max(constraints.minHeight, layoutHeight) } else { measurable.maxIntrinsicHeight(width) } } private fun calculateScaledSize(dstSize: Size): Size { return if (!useIntrinsicSize) { dstSize } else { val srcWidth = if (!painter.intrinsicSize.hasSpecifiedAndFiniteWidth()) { dstSize.width } else { painter.intrinsicSize.width } val srcHeight = if (!painter.intrinsicSize.hasSpecifiedAndFiniteHeight()) { dstSize.height } else { painter.intrinsicSize.height } val srcSize = Size(srcWidth, srcHeight) if (dstSize.width != 0f && dstSize.height != 0f) { srcSize * contentScale.computeScaleFactor(srcSize, dstSize) } else { Size.Zero } } } private fun modifyConstraints(constraints: Constraints): Constraints { val hasBoundedDimens = constraints.hasBoundedWidth && constraints.hasBoundedHeight val hasFixedDimens = constraints.hasFixedWidth && constraints.hasFixedHeight if ((!useIntrinsicSize && hasBoundedDimens) || hasFixedDimens) { // If we have fixed constraints or we are not attempting to size the // composable based on the size of the Painter, do not attempt to // modify them. Otherwise rely on Alignment and ContentScale // to determine how to position the drawing contents of the Painter within // the provided bounds return constraints.copy( minWidth = constraints.maxWidth, minHeight = constraints.maxHeight ) } val intrinsicSize = painter.intrinsicSize val intrinsicWidth = if (intrinsicSize.hasSpecifiedAndFiniteWidth()) { intrinsicSize.width.roundToInt() } else { constraints.minWidth } val intrinsicHeight = if (intrinsicSize.hasSpecifiedAndFiniteHeight()) { intrinsicSize.height.roundToInt() } else { constraints.minHeight } // Scale the width and height appropriately based on the given constraints // and ContentScale val constrainedWidth = constraints.constrainWidth(intrinsicWidth) val constrainedHeight = constraints.constrainHeight(intrinsicHeight) val scaledSize = calculateScaledSize( Size(constrainedWidth.toFloat(), constrainedHeight.toFloat()) ) // For both width and height constraints, consume the minimum of the scaled width // and the maximum constraint as some scale types can scale larger than the maximum // available size (ex ContentScale.Crop) // In this case the larger of the 2 dimensions is used and the aspect ratio is // maintained. Even if the size of the composable is smaller, the painter will // draw its content clipped val minWidth = constraints.constrainWidth(scaledSize.width.roundToInt()) val minHeight = constraints.constrainHeight(scaledSize.height.roundToInt()) return constraints.copy(minWidth = minWidth, minHeight = minHeight) } private var layerPaint: Paint? = null private fun obtainPaint(): Paint { var target = layerPaint if (target == null) { target = Paint() layerPaint = target } return target } override fun ContentDrawScope.draw() { val intrinsicSize = painter.intrinsicSize val srcWidth = if (intrinsicSize.hasSpecifiedAndFiniteWidth()) { intrinsicSize.width } else { size.width } val srcHeight = if (intrinsicSize.hasSpecifiedAndFiniteHeight()) { intrinsicSize.height } else { size.height } val srcSize = Size(srcWidth, srcHeight) // Compute the offset to translate the content based on the given alignment // and size to draw based on the ContentScale parameter val scaledSize = if (size.width != 0f && size.height != 0f) { srcSize * contentScale.computeScaleFactor(srcSize, size) } else { Size.Zero } val alignedPosition = alignment.align( IntSize(scaledSize.width.roundToInt(), scaledSize.height.roundToInt()), IntSize(size.width.roundToInt(), size.height.roundToInt()), layoutDirection ) val dx = alignedPosition.x.toFloat() val dy = alignedPosition.y.toFloat() // Only translate the current drawing position while delegating the Painter to draw // with scaled size. // Individual Painter implementations should be responsible for scaling their drawing // content accordingly to fit within the drawing area. translate(dx, dy) { if (markerColor.isSpecified) { drawIntoCanvas { val layerRect = Rect(Offset.Zero, scaledSize) val markerOffset = Offset(scaledSize.width / 2 + 6.dp.toPx(), scaledSize.height / 2 - 8.dp.toPx()) it.withSaveLayer(layerRect, obtainPaint()) { with(painter) { draw(size = scaledSize, alpha = alpha, colorFilter = colorFilter) } drawCircle( Color.White, 4.5.dp.toPx(), markerOffset, blendMode = BlendMode.Clear ) drawCircle( markerColor, 3.5.dp.toPx(), markerOffset, ) } } } else { with(painter) { draw(size = scaledSize, alpha = alpha, colorFilter = colorFilter) } } } // Maintain the same pattern as Modifier.drawBehind to allow chaining of DrawModifiers drawContent() } private fun Size.hasSpecifiedAndFiniteWidth() = this != Size.Unspecified && width.isFinite() private fun Size.hasSpecifiedAndFiniteHeight() = this != Size.Unspecified && height.isFinite() override fun hashCode(): Int { var result = painter.hashCode() result = 31 * result + sizeToIntrinsics.hashCode() result = 31 * result + alignment.hashCode() result = 31 * result + contentScale.hashCode() result = 31 * result + alpha.hashCode() result = 31 * result + (colorFilter?.hashCode() ?: 0) return result } override fun equals(other: Any?): Boolean { val otherModifier = other as? PainterWithMarkerModifier ?: return false return painter == otherModifier.painter && sizeToIntrinsics == otherModifier.sizeToIntrinsics && alignment == otherModifier.alignment && contentScale == otherModifier.contentScale && alpha == otherModifier.alpha && colorFilter == otherModifier.colorFilter } override fun toString(): String = "PainterWithMarkerModifier(painter=$painter, sizeToIntrinsics=$sizeToIntrinsics, alignment=$alignment, alpha=$alpha, colorFilter=$colorFilter, markerColor=$markerColor)" } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/PointerInput.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput fun Modifier.onHover(onHover: (Boolean) -> Unit): Modifier = this.pointerInput(Unit) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() when (event.type) { PointerEventType.Enter -> onHover(true) PointerEventType.Exit -> onHover(false) } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ProgressBar.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.theme.LightTheme class ProgressBarColors( override val normalAreaColors: AreaColors, val indeterminateAreaColors: AreaColors, ) : AreaProvider val LocalProgressBarColors = compositionLocalOf { LightTheme.ProgressBarColors } @Composable fun ProgressBar( progress: Float, modifier: Modifier = Modifier, colors: ProgressBarColors = LocalProgressBarColors.current, ) { val currentColors = colors.normalAreaColors Canvas( modifier.progressSemantics(progress).size(200.dp, 4.dp) ) { val strokeWidth = size.height val length = size.width drawLine( currentColors.startBackground, Offset(0f, strokeWidth / 2f), Offset(length, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) drawLine( currentColors.foreground, Offset(0f, strokeWidth / 2f), Offset(length * progress, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) } } @Composable fun ProgressBar( modifier: Modifier = Modifier, colors: ProgressBarColors = LocalProgressBarColors.current, ) { val transition = rememberInfiniteTransition() val currentOffset by transition.animateFloat( 0f, 1f, infiniteRepeatable( animation = keyframes { durationMillis = 1000 } ) ) val currentColors = colors.indeterminateAreaColors Canvas( modifier.progressSemantics().size(200.dp, 4.dp) ) { val strokeWidth = size.height val length = size.width val offset = currentOffset * length val brush = Brush.linearGradient( listOf(currentColors.startBackground, currentColors.endBackground, currentColors.startBackground), start = Offset(offset, 0f), end = Offset(offset + length, 0f), tileMode = TileMode.Repeated ) drawLine( brush, Offset(0f, strokeWidth / 2f), Offset(length, strokeWidth / 2f), strokeWidth, cap = StrokeCap.Round ) } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/RadioButton.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.Canvas import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.selectable import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.style.SelectionAreaProvider import io.kanro.compose.jetbrains.expui.theme.LightTheme class RadioButtonColors( override val normalAreaColors: AreaColors, override val selectionAreaColors: AreaColors, override val focusAreaColors: AreaColors, override val disabledAreaColors: AreaColors, ) : AreaProvider, SelectionAreaProvider, FocusAreaProvider, DisabledAreaProvider { @Composable fun provideArea(enabled: Boolean, focused: Boolean, selected: Boolean, content: @Composable () -> Unit) { val currentColors = when { !enabled -> disabledAreaColors focused -> focusAreaColors selected -> selectionAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides normalAreaColors, LocalSelectionAreaColors provides selectionAreaColors, LocalFocusAreaColors provides focusAreaColors, LocalDisabledAreaColors provides disabledAreaColors, content = content ) } } val LocalRadioButtonColors = compositionLocalOf { LightTheme.RadioButtonColors } @Composable fun RadioButton( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: RadioButtonColors = LocalRadioButtonColors.current, ) { val isFocused = remember { mutableStateOf(false) } colors.provideArea(enabled, isFocused.value, selected) { RadioButtonImpl( isFocused.value, selected, modifier = modifier.onFocusEvent { isFocused.value = it.isFocused }.selectable( selected = selected, enabled = enabled, onClick = onClick, interactionSource = interactionSource, indication = null, role = Role.RadioButton ) ) } } @Composable fun RadioButton( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: RadioButtonColors = LocalRadioButtonColors.current, content: @Composable RowScope.() -> Unit = {}, ) { val isFocused = remember { mutableStateOf(false) } colors.provideArea(enabled, isFocused.value, selected) { Row( modifier = modifier.onFocusEvent { isFocused.value = it.isFocused }.selectable( selected = selected, enabled = enabled, onClick = onClick, interactionSource = interactionSource, indication = null, role = Role.RadioButton ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { RadioButtonImpl(isFocused.value, selected) content() } } } @Composable private fun RadioButtonImpl( isFocused: Boolean, selected: Boolean, modifier: Modifier = Modifier, ) { val colors = LocalAreaColors.current Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(15.dp)) { if (isFocused) { drawCircle( colors.focusColor, radius = 9.5.dp.toPx(), ) } drawCircle( colors.startBorderColor, radius = 7.5.dp.toPx(), ) drawCircle( colors.startBackground, radius = 6.5.dp.toPx(), ) if (selected) { drawCircle( colors.foreground, radius = 2.5.dp.toPx(), ) } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/SegmentedButton.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.FocusAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.theme.LightTheme class SegmentedButtonColors( override val normalAreaColors: AreaColors, override val focusAreaColors: AreaColors, val itemNormalAreaColors: AreaColors, val itemHoverAreaColors: AreaColors, val itemPressedAreaColors: AreaColors, val itemSelectionAreaColors: AreaColors, val itemSelectedFocusAreaColors: AreaColors, ) : AreaProvider, FocusAreaProvider { @Composable fun provideArea(focused: Boolean, content: @Composable () -> Unit) { CompositionLocalProvider( LocalAreaColors provides if (focused) focusAreaColors else normalAreaColors, LocalNormalAreaColors provides normalAreaColors, LocalFocusAreaColors provides focusAreaColors, content = content ) } @Composable fun provideItemArea( selected: Boolean, focused: Boolean, hover: Boolean, pressed: Boolean, content: @Composable () -> Unit, ) { val currentColors = when { selected -> if (focused) itemSelectedFocusAreaColors else itemSelectionAreaColors pressed -> itemPressedAreaColors hover -> itemHoverAreaColors else -> itemNormalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides itemNormalAreaColors, LocalFocusAreaColors provides itemSelectedFocusAreaColors, LocalHoverAreaColors provides itemHoverAreaColors, LocalPressedAreaColors provides itemPressedAreaColors, LocalSelectionAreaColors provides itemSelectionAreaColors, content = content ) } } val LocalSegmentedButtonColors = compositionLocalOf { LightTheme.SegmentedButtonColors } @OptIn(ExperimentalComposeUiApi::class) @Composable fun SegmentedButton( itemCount: Int, selectedIndex: Int, onValueChange: ((Int) -> Unit)? = null, modifier: Modifier = Modifier, colors: SegmentedButtonColors = LocalSegmentedButtonColors.current, valueRender: @Composable BoxScope.(Int) -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused = interactionSource.collectIsFocusedAsState() val parentFocusRequester = remember { FocusRequester() } colors.provideArea(isFocused.value) { val areaColors = LocalAreaColors.current Row( modifier.selectableGroup().drawWithCache { onDrawBehind { if (isFocused.value) { drawRoundRect( areaColors.focusColor, size = Size(size.width + 4.dp.toPx(), size.height + 4.dp.toPx()), topLeft = Offset(-2.dp.toPx(), -2.dp.toPx()), cornerRadius = CornerRadius(5.dp.toPx()) ) } drawRoundRect(areaColors.startBorderColor, cornerRadius = CornerRadius(3.dp.toPx())) drawRoundRect( areaColors.startBackground, size = Size(size.width - 2.dp.toPx(), size.height - 2.dp.toPx()), topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), cornerRadius = CornerRadius(2.dp.toPx()) ) } }.onKeyEvent { if (it.type != KeyEventType.KeyUp) return@onKeyEvent false if (!isFocused.value) return@onKeyEvent false when (it.key) { Key.DirectionLeft -> { val target = selectedIndex - 1 if (target in 0 until itemCount) { onValueChange?.invoke(target) } return@onKeyEvent true } Key.DirectionRight -> { val target = selectedIndex + 1 if (target in 0 until itemCount) { onValueChange?.invoke(target) } return@onKeyEvent true } } false }.focusRequester(parentFocusRequester).focusable(true, interactionSource = interactionSource), horizontalArrangement = Arrangement.spacedBy((-1).dp) ) { repeat(itemCount) { val itemInteractionSource = remember(it) { MutableInteractionSource() } val hover = itemInteractionSource.collectIsHoveredAsState() val pressed = itemInteractionSource.collectIsPressedAsState() colors.provideItemArea(selectedIndex == it, isFocused.value, hover.value, pressed.value) { val current = LocalAreaColors.current val focusManager = LocalFocusManager.current Box( modifier = Modifier.drawWithCache { onDrawBehind { drawRoundRect(current.startBorderColor, cornerRadius = CornerRadius(3.dp.toPx())) drawRoundRect( current.startBackground, size = Size(size.width - 2.dp.toPx(), size.height - 2.dp.toPx()), topLeft = Offset(1.dp.toPx(), 1.dp.toPx()), cornerRadius = CornerRadius(2.dp.toPx()) ) } }.focusProperties { this.canFocus = false }.selectable(selectedIndex == it, onClick = { onValueChange?.invoke(it) if (!isFocused.value) { focusManager.clearFocus() } }, interactionSource = itemInteractionSource, indication = null, role = Role.RadioButton) ) { Box(modifier = Modifier.padding(13.dp, 4.dp)) { valueRender(it) } } } } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Tab.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectable import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider import io.kanro.compose.jetbrains.expui.style.InactiveSelectionAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme class TabColors( override val normalAreaColors: AreaColors, override val selectionAreaColors: AreaColors, override val hoverAreaColors: AreaColors, override val pressedAreaColors: AreaColors, override val inactiveAreaColors: AreaColors, override val inactiveSelectionAreaColors: AreaColors, ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, InactiveSelectionAreaProvider { @Composable fun provideArea(selected: Boolean, content: @Composable () -> Unit) { val activated = LocalContentActivated.current val currentColors = when { selected -> if (activated) selectionAreaColors else inactiveSelectionAreaColors !activated -> inactiveAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides normalAreaColors, LocalHoverAreaColors provides hoverAreaColors, LocalPressedAreaColors provides pressedAreaColors, LocalSelectionInactiveAreaColors provides inactiveSelectionAreaColors, LocalInactiveAreaColors provides inactiveAreaColors, LocalSelectionAreaColors provides selectionAreaColors, content = content ) } } val LocalTabColors = compositionLocalOf { LightTheme.TabColors } val LocalCloseableTabColors = compositionLocalOf { LightTheme.CloseableTabColors } @Composable fun Tab( selected: Boolean, onSelected: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: TabColors = LocalTabColors.current, content: @Composable RowScope.() -> Unit, ) { colors.provideArea(selected) { val currentColors = LocalAreaColors.current Box( modifier.areaBackground().drawWithCache { onDrawWithContent { drawContent() if (selected) { val strokeWidth = 3.dp.toPx() val start = Offset(strokeWidth / 2f, size.height - (strokeWidth / 2f)) val end = start.copy(x = size.width - strokeWidth / 2f) drawLine(currentColors.focusColor, start, end, strokeWidth, cap = StrokeCap.Round) } } }.focusProperties { canFocus = false }.selectable( selected = selected, enabled = true, onClick = onSelected, role = Role.Tab, interactionSource = interactionSource, indication = HoverOrPressedIndication(RectangleShape), ).padding(horizontal = 12.dp) ) { Row(modifier = Modifier.align(Alignment.Center), content = content) } } } @Composable fun CloseableTab( selected: Boolean, onSelected: () -> Unit, onClosed: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: TabColors = LocalCloseableTabColors.current, content: @Composable RowScope.() -> Unit, ) { colors.provideArea(selected) { val currentColors = LocalAreaColors.current var hover by remember { mutableStateOf(false) } Box( modifier.areaBackground().drawWithCache { onDrawWithContent { drawContent() if (selected) { val strokeWidth = 3.dp.toPx() val start = Offset(strokeWidth / 2f, size.height - (strokeWidth / 2f)) val end = start.copy(x = size.width - strokeWidth / 2f) drawLine(currentColors.focusColor, start, end, strokeWidth, cap = StrokeCap.Round) } } }.focusProperties { canFocus = false }.selectable( selected = selected, enabled = true, onClick = onSelected, role = Role.Tab, interactionSource = interactionSource, indication = HoverOrPressedIndication(RectangleShape), ).onHover { hover = it }.padding(horizontal = 12.dp) ) { Row( modifier = Modifier.align(Alignment.Center).graphicsLayer(alpha = if (hover || selected) 1f else 0.7f), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { content() CloseButton(hover || selected, onClosed) } } } } @Composable private fun CloseButton( shown: Boolean, onClosed: () -> Unit, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { var hover by remember { mutableStateOf(false) } Box( modifier = Modifier.size(16.dp).areaBackground().focusProperties { canFocus = false }.clickable( enabled = shown, onClick = onClosed, role = Role.Button, interactionSource = interactionSource, indication = null, ).onHover { hover = it } ) { if (shown) { Icon( if (hover) "icons/closeSmallHovered.svg" else "icons/closeSmall.svg", contentDescription = "Close" ) } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/TextArea.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.style.areaBorder import io.kanro.compose.jetbrains.expui.style.areaFocusBorder import kotlin.math.max @Composable fun TextArea( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalDefaultTextStyle.current, placeholder: @Composable (() -> Unit)? = null, footer: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RectangleShape, colors: TextFieldColors = LocalTextFieldColors.current, ) { val focused = interactionSource.collectIsFocusedAsState() colors.provideArea(enabled, focused.value, isError) { val currentColors = LocalAreaColors.current val textColor = textStyle.color.takeOrElse { currentColors.text } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier.defaultMinSize(minWidth = 270.dp, minHeight = 55.dp), enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = false, maxLines = maxLines, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = SolidColor(currentColors.text), ) { TextAreaDecorationBox( focused = focused.value, shape = shape, innerTextField = it, placeholder = if (value.isEmpty()) placeholder else null, footer = footer ) } } } @Composable private fun TextAreaDecorationBox( focused: Boolean, shape: Shape, innerTextField: @Composable () -> Unit, placeholder: @Composable (() -> Unit)? = null, footer: @Composable (() -> Unit)? = null, ) { Layout( modifier = Modifier.areaBackground(shape = shape).areaFocusBorder(focused, shape = shape) .areaBorder(shape = shape), content = { if (footer != null) { Row(modifier = Modifier.layoutId(FooterId), horizontalArrangement = Arrangement.Start) { Box(modifier = Modifier.padding(horizontal = 6.dp)) { footer() } } } if (placeholder != null) { Box(modifier = Modifier.layoutId(PlaceholderId), contentAlignment = Alignment.Center) { placeholder() } } Box(modifier = Modifier.layoutId(TextFieldId), propagateMinConstraints = true) { innerTextField() } } ) { measurables, incomingConstraints -> // used to calculate the constraints for measuring elements that will be placed in a row var occupiedSpaceVertically = 0 val constraintsWithoutPadding = incomingConstraints.offset( horizontal = -2 * TextAreaPadding.roundToPx(), vertical = -2 * TextAreaPadding.roundToPx() ) val footerConstraints = constraintsWithoutPadding.copy(minWidth = 0, minHeight = 0) val footerPlaceable = measurables.find { it.layoutId == FooterId }?.measure(footerConstraints) occupiedSpaceVertically += footerPlaceable?.height ?: 0 val textConstraints = constraintsWithoutPadding.offset( vertical = -occupiedSpaceVertically ).copy(minWidth = 0) val textFieldPlaceable = measurables.first { it.layoutId == TextFieldId }.measure(textConstraints) // measure placeholder val placeholderConstraints = textConstraints.copy(minHeight = 0) val placeholderPlaceable = measurables.find { it.layoutId == PlaceholderId }?.measure(placeholderConstraints) val width = calculateWidth( footerPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints ) + 2 * TextAreaPadding.roundToPx() val height = calculateHeight( footerPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints ) + 2 * TextAreaPadding.roundToPx() layout(width, height) { place( height, width, footerPlaceable, textFieldPlaceable, placeholderPlaceable, this@Layout ) } } } private fun calculateWidth( footerPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, ): Int { return maxOf( footerPlaceable?.width ?: 0, textFieldPlaceable.width, placeholderPlaceable?.width ?: 0, constraints.minWidth ) } private fun calculateHeight( footerPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, ): Int { val middleSection = maxOf( textFieldPlaceable.height, placeholderPlaceable?.height ?: 0 ) val wrappedHeight = (footerPlaceable?.height ?: 0) + middleSection return max(wrappedHeight, constraints.minHeight) } private fun Placeable.PlacementScope.place( height: Int, width: Int, footerPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, density: Density, ) = with(density) { val padding = TextAreaPadding.roundToPx() // placed center vertically and to the start edge horizontally footerPlaceable?.placeRelative( 0, height - footerPlaceable.height ) // placed center vertically and after the leading icon horizontally if single line text field // placed to the top with padding for multi line text field textFieldPlaceable.placeRelative(padding, padding) // placed similar to the input text above placeholderPlaceable?.placeRelative(padding, padding) } private const val PlaceholderId = "Placeholder" private const val TextFieldId = "TextField" private const val FooterId = "Footer" private val TextAreaPadding = 6.dp ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/TextField.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.ErrorFocusAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalErrorAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.style.areaBorder import io.kanro.compose.jetbrains.expui.style.areaFocusBorder import io.kanro.compose.jetbrains.expui.theme.LightTheme import kotlin.math.max data class TextFieldColors( override val normalAreaColors: AreaColors, override val errorAreaColors: AreaColors, override val disabledAreaColors: AreaColors, override val errorFocusAreaColors: AreaColors, override val focusAreaColors: AreaColors, ) : AreaProvider, DisabledAreaProvider, ErrorFocusAreaProvider { @Composable fun provideArea(enabled: Boolean, focused: Boolean, isError: Boolean, content: @Composable () -> Unit) { val currentColors = when { !enabled -> disabledAreaColors isError -> if (focused) errorFocusAreaColors else errorAreaColors focused -> focusAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalDisabledAreaColors provides disabledAreaColors, LocalErrorAreaColors provides errorAreaColors, LocalFocusAreaColors provides focusAreaColors, LocalErrorAreaColors provides errorAreaColors, LocalNormalAreaColors provides normalAreaColors, content = content ) } } val LocalTextFieldColors = compositionLocalOf { LightTheme.TextFieldColors } @Composable fun TextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalDefaultTextStyle.current, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(3.dp), colors: TextFieldColors = LocalTextFieldColors.current, ) { val focused = interactionSource.collectIsFocusedAsState() colors.provideArea(enabled, focused.value, isError) { val currentColors = LocalAreaColors.current val textColor = textStyle.color.takeOrElse { currentColors.text } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier.defaultMinSize(minWidth = 64.dp), enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = true, maxLines = 1, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = SolidColor(currentColors.text), ) { TextFieldDecorationBox( focused = focused.value, shape = shape, innerTextField = it, placeholder = if (value.isEmpty()) placeholder else null, leadingIcon = leadingIcon, trailingIcon = trailingIcon, ) } } } @Composable fun TextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalDefaultTextStyle.current, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(3.dp), colors: TextFieldColors = LocalTextFieldColors.current, ) { val focused = interactionSource.collectIsFocusedAsState() colors.provideArea(enabled, focused.value, isError) { val currentColors = LocalAreaColors.current val textColor = textStyle.color.takeOrElse { currentColors.text } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier.defaultMinSize(minWidth = 64.dp), enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = true, maxLines = 1, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = SolidColor(currentColors.text), ) { TextFieldDecorationBox( focused = focused.value, shape = shape, innerTextField = it, placeholder = if (value.text.isEmpty()) placeholder else null, leadingIcon = leadingIcon, trailingIcon = trailingIcon, ) } } } @Composable private fun TextFieldDecorationBox( focused: Boolean, shape: Shape, innerTextField: @Composable () -> Unit, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, ) { Layout( modifier = Modifier.areaBackground(shape = shape).areaFocusBorder(focused, shape = shape) .areaBorder(shape = shape), content = { if (leadingIcon != null) { Box(modifier = Modifier.layoutId(LeadingId), contentAlignment = Alignment.Center) { leadingIcon() } } if (trailingIcon != null) { Box(modifier = Modifier.layoutId(TrailingId), contentAlignment = Alignment.Center) { trailingIcon() } } if (placeholder != null) { Box(modifier = Modifier.layoutId(PlaceholderId), contentAlignment = Alignment.Center) { placeholder() } } Box(modifier = Modifier.layoutId(TextFieldId), propagateMinConstraints = true) { innerTextField() } } ) { measurables, incomingConstraints -> // used to calculate the constraints for measuring elements that will be placed in a row var occupiedSpaceHorizontally = 0 val constraintsWithoutPadding = incomingConstraints.offset( horizontal = -2 * HorizontalTextFieldPadding.roundToPx(), vertical = -2 * VerticalTextFieldPadding.roundToPx() ) val iconsConstraints = constraintsWithoutPadding.copy(minWidth = 0, minHeight = 0) // measure leading icon val leadingPlaceable = measurables.find { it.layoutId == LeadingId }?.measure(iconsConstraints) occupiedSpaceHorizontally += leadingPlaceable?.width ?: 0 // measure trailing icon val trailingPlaceable = measurables.find { it.layoutId == TrailingId } ?.measure(iconsConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += trailingPlaceable?.width ?: 0 val textConstraints = constraintsWithoutPadding.offset( horizontal = -occupiedSpaceHorizontally ).copy(minHeight = 0) val textFieldPlaceable = measurables.first { it.layoutId == TextFieldId }.measure(textConstraints) // measure placeholder val placeholderConstraints = textConstraints.copy(minWidth = 0) val placeholderPlaceable = measurables.find { it.layoutId == PlaceholderId }?.measure(placeholderConstraints) val width = calculateWidth( leadingPlaceable, trailingPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints ) + 2 * HorizontalTextFieldPadding.roundToPx() val height = calculateHeight( leadingPlaceable, trailingPlaceable, textFieldPlaceable, placeholderPlaceable, incomingConstraints ) + 2 * VerticalTextFieldPadding.roundToPx() layout(width, height) { place( height, width, leadingPlaceable, trailingPlaceable, textFieldPlaceable, placeholderPlaceable, this@Layout ) } } } private fun calculateWidth( leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, ): Int { val middleSection = maxOf( textFieldPlaceable.width, placeholderPlaceable?.width ?: 0 ) val wrappedWidth = (leadingPlaceable?.width ?: 0) + middleSection + (trailingPlaceable?.width ?: 0) return max(wrappedWidth, constraints.minWidth) } private fun calculateHeight( leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, constraints: Constraints, ): Int { return maxOf( leadingPlaceable?.height ?: 0, textFieldPlaceable.height, placeholderPlaceable?.height ?: 0, trailingPlaceable?.height ?: 0, constraints.minHeight ) } private fun Placeable.PlacementScope.place( height: Int, width: Int, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, density: Density, ) = with(density) { val horizontalPadding = HorizontalTextFieldPadding.roundToPx() // placed center vertically and to the start edge horizontally leadingPlaceable?.placeRelative( horizontalPadding, Alignment.CenterVertically.align(leadingPlaceable.height, height) ) // placed center vertically and to the end edge horizontally trailingPlaceable?.placeRelative( width - trailingPlaceable.width - horizontalPadding, Alignment.CenterVertically.align(trailingPlaceable.height, height) ) // placed center vertically and after the leading icon horizontally if single line text field // placed to the top with padding for multi line text field textFieldPlaceable.placeRelative( horizontalPadding + (leadingPlaceable?.width ?: 0), Alignment.CenterVertically.align(textFieldPlaceable.height, height) ) // placed similar to the input text above placeholderPlaceable?.let { it.placeRelative( horizontalPadding + (leadingPlaceable?.width ?: 0), Alignment.CenterVertically.align(it.height, height) ) } } private const val PlaceholderId = "Placeholder" private const val TextFieldId = "TextField" private const val LeadingId = "Leading" private const val TrailingId = "Trailing" private val HorizontalTextFieldPadding = 6.dp private val VerticalTextFieldPadding = 3.dp ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/ToolBar.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.Indication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.DisabledAreaProvider import io.kanro.compose.jetbrains.expui.style.HoverAreaProvider import io.kanro.compose.jetbrains.expui.style.InactiveSelectionAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.PressedAreaProvider import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme class ToolBarActionButtonColors( override val normalAreaColors: AreaColors, override val hoverAreaColors: AreaColors, override val pressedAreaColors: AreaColors, override val disabledAreaColors: AreaColors, override val selectionAreaColors: AreaColors, override val inactiveAreaColors: AreaColors, override val inactiveSelectionAreaColors: AreaColors, ) : AreaProvider, HoverAreaProvider, PressedAreaProvider, DisabledAreaProvider, InactiveSelectionAreaProvider { @Composable fun provideArea(enabled: Boolean, selected: Boolean, content: @Composable () -> Unit) { val activated = LocalContentActivated.current val currentColors = when { !enabled -> disabledAreaColors selected -> if (activated) selectionAreaColors else inactiveSelectionAreaColors !activated -> inactiveAreaColors else -> normalAreaColors } CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides normalAreaColors, LocalDisabledAreaColors provides disabledAreaColors, LocalHoverAreaColors provides hoverAreaColors, LocalPressedAreaColors provides pressedAreaColors, LocalSelectionInactiveAreaColors provides inactiveSelectionAreaColors, LocalInactiveAreaColors provides inactiveAreaColors, LocalSelectionAreaColors provides selectionAreaColors, content = content ) } } val LocalToolBarActionButtonColors = compositionLocalOf { LightTheme.ToolBarActionButtonColors } @Composable fun ToolBarActionButton( selected: Boolean = false, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = RoundedCornerShape(6.dp), indication: Indication? = HoverOrPressedIndication(shape), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: ToolBarActionButtonColors = LocalToolBarActionButtonColors.current, content: @Composable BoxScope.() -> Unit, ) { colors.provideArea(enabled, selected) { Box( modifier.clickable( interactionSource = interactionSource, indication = indication, enabled = enabled, onClick = onClick, role = Role.Button ).areaBackground(shape = shape), propagateMinConstraints = true ) { content() } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/control/Tooltip.kt ================================================ package io.kanro.compose.jetbrains.expui.control import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.theme.LocalIsDarkTheme class ToolTipColors( val isDark: Boolean, override val normalAreaColors: AreaColors, ) : AreaProvider { @Composable fun provideArea(content: @Composable () -> Unit) { CompositionLocalProvider( LocalAreaColors provides normalAreaColors, LocalNormalAreaColors provides normalAreaColors, LocalIsDarkTheme provides isDark, content = content ) } } val LocalToolTipColors = compositionLocalOf { LightTheme.ToolTipColors } @Composable @OptIn(ExperimentalFoundationApi::class) fun Tooltip( tooltip: @Composable () -> Unit, modifier: Modifier = Modifier, delayMillis: Int = 500, tooltipPlacement: TooltipPlacement = TooltipPlacement.CursorPoint( offset = DpOffset(0.dp, 32.dp) ), colors: ToolTipColors = LocalToolTipColors.current, content: @Composable () -> Unit, ) { TooltipArea( { colors.provideArea { Box( modifier = Modifier.shadow(8.dp).areaBackground() .border(1.dp, LocalAreaColors.current.startBorderColor), contentAlignment = Alignment.Center ) { Box( modifier = Modifier.padding(16.dp), ) { tooltip() } } } }, modifier, delayMillis, tooltipPlacement, content ) } @Composable @OptIn(ExperimentalFoundationApi::class) fun Tooltip( tooltip: String, modifier: Modifier = Modifier, delayMillis: Int = 500, tooltipPlacement: TooltipPlacement = TooltipPlacement.CursorPoint( offset = DpOffset(0.dp, 32.dp) ), colors: ToolTipColors = LocalToolTipColors.current, content: @Composable () -> Unit, ) { Tooltip({ Label(tooltip) }, modifier, delayMillis, tooltipPlacement, colors, content) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/AreaColors.kt ================================================ package io.kanro.compose.jetbrains.expui.style import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.isUnspecified import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.kanro.compose.jetbrains.expui.theme.LightTheme /** * Color definition for an area which has background and foreground. */ data class AreaColors( /** * Text foreground color. */ val text: Color, /** * Overriding the foreground colour for some components that have their own colour like [Icon][io.kanro.compose.jetbrains.expui.control.Icon]. */ val foreground: Color, val startBackground: Color, val endBackground: Color, val startBorderColor: Color, val endBorderColor: Color, val focusColor: Color, ) @Composable fun Modifier.areaBackground(areaColors: AreaColors = LocalAreaColors.current, shape: Shape = RectangleShape): Modifier { return background(areaColors, shape) } fun Modifier.background(areaColors: AreaColors, shape: Shape = RectangleShape): Modifier { if (areaColors.startBackground.isUnspecified) { return this } if (areaColors.endBackground.isUnspecified || areaColors.startBackground == areaColors.endBackground) { return this.background(areaColors.startBackground, shape) } return this.background(Brush.linearGradient(listOf(areaColors.startBackground, areaColors.endBackground)), shape) } @Composable fun Modifier.areaBorder( areaColors: AreaColors = LocalAreaColors.current, width: Dp = 1.dp, shape: Shape = RectangleShape, ): Modifier { return border(areaColors, width, shape) } fun Modifier.border(areaColors: AreaColors, width: Dp = 1.dp, shape: Shape = RectangleShape): Modifier { if (areaColors.startBorderColor.isUnspecified) { return this } if (areaColors.endBorderColor.isUnspecified || areaColors.startBorderColor == areaColors.endBorderColor) { return this.border(width, areaColors.startBorderColor, shape) } return this.border(width, Brush.linearGradient(listOf(areaColors.startBackground, areaColors.endBackground)), shape) } @Composable fun Modifier.areaFocusBorder( focused: Boolean, areaColors: AreaColors = LocalAreaColors.current, width: Dp = 2.dp, shape: Shape = RectangleShape, ): Modifier { return focusBorder(focused, areaColors, width, shape) } fun Modifier.focusBorder( focused: Boolean, areaColors: AreaColors, width: Dp = 2.dp, shape: Shape = RectangleShape, ): Modifier { if (!focused) return this if (areaColors.focusColor.isUnspecified) { return this } return this.outerBorder(width, areaColors.focusColor, shape) } val LocalAreaColors = compositionLocalOf { LightTheme.NormalAreaColors } val LocalNormalAreaColors = compositionLocalOf { LightTheme.NormalAreaColors } val LocalInactiveAreaColors = compositionLocalOf { LightTheme.InactiveAreaColors } val LocalErrorAreaColors = compositionLocalOf { LightTheme.ErrorAreaColors } val LocalErrorInactiveAreaColors = compositionLocalOf { LightTheme.ErrorInactiveAreaColors } val LocalDisabledAreaColors = compositionLocalOf { LightTheme.DisabledAreaColors } val LocalHoverAreaColors = compositionLocalOf { LightTheme.HoverAreaColors } val LocalPressedAreaColors = compositionLocalOf { LightTheme.PressedAreaColors } val LocalFocusAreaColors = compositionLocalOf { LightTheme.FocusAreaColors } val LocalSelectionAreaColors = compositionLocalOf { LightTheme.SelectionAreaColors } val LocalSelectionInactiveAreaColors = compositionLocalOf { LightTheme.SelectionInactiveAreaColors } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/AreaProvider.kt ================================================ package io.kanro.compose.jetbrains.expui.style interface AreaProvider { val normalAreaColors: AreaColors } interface ErrorAreProvider : AreaProvider { val errorAreaColors: AreaColors } interface ErrorFocusAreaProvider : ErrorAreProvider, FocusAreaProvider { val errorFocusAreaColors: AreaColors } interface SelectionAreaProvider : AreaProvider { val selectionAreaColors: AreaColors } interface FocusAreaProvider : AreaProvider { val focusAreaColors: AreaColors } interface DisabledAreaProvider : AreaProvider { val disabledAreaColors: AreaColors } interface HoverAreaProvider : AreaProvider { val hoverAreaColors: AreaColors } interface PressedAreaProvider : AreaProvider { val pressedAreaColors: AreaColors } interface InactiveAreaProvider : AreaProvider { val inactiveAreaColors: AreaColors } interface InactiveErrorAreaProvider : ErrorAreProvider, InactiveAreaProvider { val inactiveErrorAreaColors: AreaColors } interface InactiveSelectionAreaProvider : SelectionAreaProvider, InactiveAreaProvider { val inactiveSelectionAreaColors: AreaColors } interface InactiveFocusAreaProvider : FocusAreaProvider, InactiveAreaProvider { val inactiveFocusAreaColors: AreaColors } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/Border.kt ================================================ package io.kanro.compose.jetbrains.expui.style import androidx.compose.foundation.BorderStroke import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isSimple import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathOperation import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize import kotlin.math.ceil fun Modifier.outerBorder(border: BorderStroke, shape: Shape = RectangleShape) = outerBorder(width = border.width, brush = border.brush, shape = shape) fun Modifier.outerBorder(width: Dp, color: Color, shape: Shape = RectangleShape) = outerBorder(width, SolidColor(color), shape) fun Modifier.outerBorder(width: Dp, brush: Brush, shape: Shape): Modifier = composed( factory = { // BorderCache object that is lazily allocated depending on the type of shape // This object is only used for generic shapes and rounded rectangles with different corner // radius sizes. val borderCacheRef = remember { Ref() } this.then( Modifier.drawWithCache { val hasValidBorderParams = width.toPx() >= 0f && size.minDimension > 0f if (!hasValidBorderParams) { drawContentWithoutBorder() } else { val strokeWidthPx = if (width == Dp.Hairline) 1f else ceil(width.toPx()) val halfStroke = strokeWidthPx / 2 val topLeft = Offset(-halfStroke, -halfStroke) val borderSize = Size( size.width + strokeWidthPx, size.height + strokeWidthPx ) when (val outline = shape.createOutline(size, layoutDirection, this)) { is Outline.Generic -> TODO("Not support for generic outline") is Outline.Rounded -> drawRoundRectBorder( borderCacheRef, brush, outline, topLeft, borderSize, strokeWidthPx ) is Outline.Rectangle -> drawRectBorder( brush, topLeft, borderSize, strokeWidthPx ) } } } ) }, inspectorInfo = debugInspectorInfo { name = "outerBorder" properties["width"] = width if (brush is SolidColor) { properties["color"] = brush.value value = brush.value } else { properties["brush"] = brush } properties["shape"] = shape } ) private fun Ref.obtain(): BorderCache = this.value ?: BorderCache().also { value = it } private data class BorderCache( private var imageBitmap: ImageBitmap? = null, private var canvas: androidx.compose.ui.graphics.Canvas? = null, private var canvasDrawScope: CanvasDrawScope? = null, private var borderPath: Path? = null, ) { inline fun CacheDrawScope.drawBorderCache( borderSize: IntSize, config: ImageBitmapConfig, block: DrawScope.() -> Unit, ): ImageBitmap { var targetImageBitmap = imageBitmap var targetCanvas = canvas // If we previously had allocated a full Argb888 ImageBitmap but are only requiring // an alpha mask, just re-use the same ImageBitmap instead of allocating a new one val compatibleConfig = targetImageBitmap?.config == ImageBitmapConfig.Argb8888 || config == targetImageBitmap?.config if (targetImageBitmap == null || targetCanvas == null || size.width > targetImageBitmap.width || size.height > targetImageBitmap.height || !compatibleConfig) { targetImageBitmap = ImageBitmap( borderSize.width, borderSize.height, config = config ).also { imageBitmap = it } targetCanvas = androidx.compose.ui.graphics.Canvas(targetImageBitmap).also { canvas = it } } val targetDrawScope = canvasDrawScope ?: CanvasDrawScope().also { canvasDrawScope = it } val drawSize = borderSize.toSize() targetDrawScope.draw( this, layoutDirection, targetCanvas, drawSize ) { // Clear the previously rendered portion within this ImageBitmap as we could // be re-using it drawRect( color = Color.Black, size = drawSize, blendMode = BlendMode.Clear ) block() } targetImageBitmap.prepareToDraw() return targetImageBitmap } fun obtainPath(): Path = borderPath ?: Path().also { borderPath = it } } /** * Border implementation for invalid parameters that just draws the content * as the given border parameters are infeasible (ex. negative border width) */ private fun CacheDrawScope.drawContentWithoutBorder(): DrawResult = onDrawWithContent { drawContent() } /** * Border implementation for simple rounded rects and those with different corner * radii */ private fun CacheDrawScope.drawRoundRectBorder( borderCacheRef: Ref, brush: Brush, outline: Outline.Rounded, topLeft: Offset, borderSize: Size, strokeWidth: Float, ): DrawResult { return if (outline.roundRect.isSimple) { val cornerRadius = outline.roundRect.topLeftCornerRadius val halfStroke = strokeWidth / 2 val borderStroke = Stroke(strokeWidth) onDrawWithContent { drawContent() // Otherwise draw a stroked rounded rect with the corner radius // shrunk by half of the stroke width. This will ensure that the // outer curvature of the rounded rectangle will have the desired // corner radius. drawRoundRect( brush = brush, topLeft = topLeft, size = borderSize, cornerRadius = cornerRadius.expand(halfStroke), style = borderStroke ) } } else { val path = borderCacheRef.obtain().obtainPath() val roundedRectPath = createRoundRectPath(path, outline.roundRect, strokeWidth) onDrawWithContent { drawContent() drawPath(roundedRectPath, brush = brush) } } } /** * Border implementation for rectangular borders */ private fun CacheDrawScope.drawRectBorder( brush: Brush, topLeft: Offset, borderSize: Size, strokeWidthPx: Float, ): DrawResult { // If we are drawing a rectangular stroke, just offset it by half the stroke // width as strokes are always drawn centered on their geometry. // If the border is larger than the drawing area, just fill the area with a // solid rectangle val style = Stroke(strokeWidthPx) return onDrawWithContent { drawContent() drawRoundRect( brush = brush, topLeft = topLeft, size = borderSize, cornerRadius = CornerRadius(strokeWidthPx), style = style ) } } private fun createRoundRectPath( targetPath: Path, roundedRect: RoundRect, strokeWidth: Float, ): Path = targetPath.apply { reset() addRoundRect(roundedRect) val insetPath = Path().apply { addRoundRect(createInsetRoundedRect(strokeWidth, roundedRect)) } op(this, insetPath, PathOperation.Difference) } private fun createInsetRoundedRect( widthPx: Float, roundedRect: RoundRect, ) = RoundRect( left = -widthPx, top = -widthPx, right = roundedRect.width + widthPx, bottom = roundedRect.height + widthPx, topLeftCornerRadius = roundedRect.topLeftCornerRadius.expand(widthPx), topRightCornerRadius = roundedRect.topRightCornerRadius.expand(widthPx), bottomLeftCornerRadius = roundedRect.bottomLeftCornerRadius.expand(widthPx), bottomRightCornerRadius = roundedRect.bottomRightCornerRadius.expand(widthPx) ) private fun CornerRadius.expand(value: Float): CornerRadius = CornerRadius(this.x + value, this.y + value) ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/style/TextStyle.kt ================================================ package io.kanro.compose.jetbrains.expui.style import androidx.compose.runtime.compositionLocalOf import io.kanro.compose.jetbrains.expui.theme.LightTheme val LocalDefaultTextStyle = compositionLocalOf { LightTheme.DefaultTextStyle } val LocalDefaultBoldTextStyle = compositionLocalOf { LightTheme.DefaultBoldTextStyle } val LocalParagraphTextStyle = compositionLocalOf { LightTheme.ParagraphTextStyle } val LocalMediumTextStyle = compositionLocalOf { LightTheme.MediumTextStyle } val LocalMediumBoldTextStyle = compositionLocalOf { LightTheme.MediumBoldTextStyle } val LocalSmallTextStyle = compositionLocalOf { LightTheme.SmallTextStyle } val LocalH0TextStyle = compositionLocalOf { LightTheme.H0TextStyle } val LocalH1TextStyle = compositionLocalOf { LightTheme.H1TextStyle } val LocalH2TextStyle = compositionLocalOf { LightTheme.H2TextStyle } val LocalH2BoldTextStyle = compositionLocalOf { LightTheme.H2BoldTextStyle } val LocalH3TextStyle = compositionLocalOf { LightTheme.H3TextStyle } val LocalH3BoldTextStyle = compositionLocalOf { LightTheme.H3BoldTextStyle } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/theme/DarkTheme.kt ================================================ package io.kanro.compose.jetbrains.expui.theme import androidx.compose.foundation.LocalContextMenuRepresentation import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import io.kanro.compose.jetbrains.expui.control.ActionButtonColors import io.kanro.compose.jetbrains.expui.control.ButtonColors import io.kanro.compose.jetbrains.expui.control.CheckBoxColors import io.kanro.compose.jetbrains.expui.control.ComboBoxColors import io.kanro.compose.jetbrains.expui.control.ContextMenuColors import io.kanro.compose.jetbrains.expui.control.DropdownMenuColors import io.kanro.compose.jetbrains.expui.control.JbContextMenuRepresentation import io.kanro.compose.jetbrains.expui.control.LinkColors import io.kanro.compose.jetbrains.expui.control.LocalActionButtonColors import io.kanro.compose.jetbrains.expui.control.LocalCheckBoxColors import io.kanro.compose.jetbrains.expui.control.LocalCloseableTabColors import io.kanro.compose.jetbrains.expui.control.LocalComboBoxColors import io.kanro.compose.jetbrains.expui.control.LocalContextMenuColors import io.kanro.compose.jetbrains.expui.control.LocalDropdownMenuColors import io.kanro.compose.jetbrains.expui.control.LocalLinkColors import io.kanro.compose.jetbrains.expui.control.LocalOutlineButtonColors import io.kanro.compose.jetbrains.expui.control.LocalPrimaryButtonColors import io.kanro.compose.jetbrains.expui.control.LocalProgressBarColors import io.kanro.compose.jetbrains.expui.control.LocalRadioButtonColors import io.kanro.compose.jetbrains.expui.control.LocalSegmentedButtonColors import io.kanro.compose.jetbrains.expui.control.LocalTabColors import io.kanro.compose.jetbrains.expui.control.LocalTextFieldColors import io.kanro.compose.jetbrains.expui.control.LocalToolBarActionButtonColors import io.kanro.compose.jetbrains.expui.control.LocalToolTipColors import io.kanro.compose.jetbrains.expui.control.ProgressBarColors import io.kanro.compose.jetbrains.expui.control.RadioButtonColors import io.kanro.compose.jetbrains.expui.control.SegmentedButtonColors import io.kanro.compose.jetbrains.expui.control.TabColors import io.kanro.compose.jetbrains.expui.control.TextFieldColors import io.kanro.compose.jetbrains.expui.control.ToolBarActionButtonColors import io.kanro.compose.jetbrains.expui.control.ToolTipColors import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDefaultBoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalErrorAreaColors import io.kanro.compose.jetbrains.expui.style.LocalErrorInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalH0TextStyle import io.kanro.compose.jetbrains.expui.style.LocalH1TextStyle import io.kanro.compose.jetbrains.expui.style.LocalH2BoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalH2TextStyle import io.kanro.compose.jetbrains.expui.style.LocalH3BoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalH3TextStyle import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalMediumBoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalMediumTextStyle import io.kanro.compose.jetbrains.expui.style.LocalParagraphTextStyle import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSmallTextStyle import io.kanro.compose.jetbrains.expui.window.LocalMainToolBarColors import io.kanro.compose.jetbrains.expui.window.LocalWindowsCloseWindowButtonColors import io.kanro.compose.jetbrains.expui.window.MainToolBarColors object DarkTheme : Theme { val Grey1 = Color(0xFF1E1F22) val Grey2 = Color(0xFF2B2D30) val Grey3 = Color(0xFF393B40) val Grey4 = Color(0xFF43454A) val Grey5 = Color(0xFF4E5157) val Grey6 = Color(0xFF5A5D63) val Grey7 = Color(0xFF6F737A) val Grey8 = Color(0xFF868A91) val Grey9 = Color(0xFF9DA0A8) val Grey10 = Color(0xFFB4B8BF) val Grey11 = Color(0xFFCED0D6) val Grey12 = Color(0xFFDFE1E5) val Grey13 = Color(0xFFF0F1F2) val Grey14 = Color(0xFFFFFFFF) val Blue1 = Color(0xFF25324D) val Blue2 = Color(0xFF2E436E) val Blue3 = Color(0xFF35538F) val Blue4 = Color(0xFF375FAD) val Blue5 = Color(0xFF366ACE) val Blue6 = Color(0xFF3573F0) val Blue7 = Color(0xFF467FF2) val Blue8 = Color(0xFF548AF7) val Blue9 = Color(0xFF6B9BFA) val Blue10 = Color(0xFF83ACFC) val Blue11 = Color(0xFF99BBFF) val Green1 = Color(0xFF253627) val Green2 = Color(0xFF375239) val Green3 = Color(0xFF436946) val Green4 = Color(0xFF4E8052) val Green5 = Color(0xFF57965C) val Green6 = Color(0xFF5FAD65) val Green7 = Color(0xFF73BD79) val Green8 = Color(0xFF89CC8E) val Green9 = Color(0xFFA0DBA5) val Green10 = Color(0xFFB9EBBD) val Green11 = Color(0xFFD4FAD7) val Yellow1 = Color(0xFF3D3223) val Yellow2 = Color(0xFF5E4D33) val Yellow3 = Color(0xFF826A41) val Yellow4 = Color(0xFF9E814A) val Yellow5 = Color(0xFFBA9752) val Yellow6 = Color(0xFFD6AE58) val Yellow7 = Color(0xFFF2C55C) val Yellow8 = Color(0xFFF5D273) val Yellow9 = Color(0xFFF7DE8B) val Yellow10 = Color(0xFFFCEBA4) val Yellow11 = Color(0xFFFFF6BD) val Red1 = Color(0xFF402929) val Red2 = Color(0xFF5E3838) val Red3 = Color(0xFF7A4343) val Red4 = Color(0xFF9C4E4E) val Red5 = Color(0xFFBD5757) val Red6 = Color(0xFFDB5C5C) val Red7 = Color(0xFFE37774) val Red8 = Color(0xFFEB938D) val Red9 = Color(0xFFF2B1AA) val Red10 = Color(0xFFF7CCC6) val Red11 = Color(0xFFFAE3DE) val Orange1 = Color(0xFF45322B) val Orange2 = Color(0xFF614438) val Orange3 = Color(0xFF825845) val Orange4 = Color(0xFFA36B4E) val Orange5 = Color(0xFFC27A53) val Orange6 = Color(0xFFE08855) val Orange7 = Color(0xFFE5986C) val Orange8 = Color(0xFFF0AC81) val Orange9 = Color(0xFFF5BD98) val Orange10 = Color(0xFFFACEAF) val Orange11 = Color(0xFFFFDFC7) override val isDark: Boolean = true val NormalAreaColors = AreaColors( text = Grey12, foreground = Color.Unspecified, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey1, endBorderColor = Grey1, focusColor = Blue6, ) val InactiveAreaColors = NormalAreaColors.copy( text = Grey6 ) val ErrorAreaColors = NormalAreaColors.copy( text = Red6 ) val ErrorInactiveAreaColors = ErrorAreaColors val DisabledAreaColors = NormalAreaColors.copy( text = Grey6, foreground = Grey6, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey4, endBorderColor = Grey4 ) val HoverAreaColors = NormalAreaColors.copy( startBackground = Grey3, endBackground = Grey3, ) val PressedAreaColors = NormalAreaColors.copy( startBackground = Grey5, endBackground = Grey5, ) val FocusAreaColors = NormalAreaColors.copy() val SelectionAreaColors = NormalAreaColors.copy( startBackground = Blue2, endBackground = Blue2, ) val SelectionInactiveAreaColors = NormalAreaColors.copy( startBackground = Grey4, endBackground = Grey4, ) val MainToolBarColors = MainToolBarColors( isDark = true, normalAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey1, endBorderColor = Grey1, focusColor = Blue6, ), inactiveAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey3, endBackground = Grey3, startBorderColor = Grey1, endBorderColor = Grey1, focusColor = Blue6, ), actionButtonColors = ActionButtonColors( normalAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), hoverAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey3, endBackground = Grey3, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), pressedAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey3, endBackground = Grey3, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), disabledAreaColors = AreaColors( text = Grey13, foreground = Grey6, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), ) ) val WindowsCloseWindowButtonColors = ActionButtonColors( normalAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), hoverAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Red6, endBackground = Red6, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Red6, ), pressedAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Red6, endBackground = Red6, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Red6, ), disabledAreaColors = AreaColors( text = Grey13, foreground = Grey6, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), ) val ActionButtonColors = ActionButtonColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Grey3, endBackground = Grey3, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Grey5, endBackground = Grey5, ), disabledAreaColors = DisabledAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ) ) val CheckBoxColors = CheckBoxColors( normalAreaColors = NormalAreaColors.copy( foreground = Grey14, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), selectionAreaColors = SelectionAreaColors.copy( foreground = Grey14, startBackground = Blue6, endBackground = Blue6, startBorderColor = Blue6, endBorderColor = Blue6, ), focusAreaColors = FocusAreaColors.copy( foreground = Grey14, startBackground = Blue6, endBackground = Blue6, startBorderColor = Color(0xFF101012), endBorderColor = Color(0xFF101012), ), disabledAreaColors = DisabledAreaColors.copy( foreground = Grey5, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ) ) val RadioButtonColors = RadioButtonColors( normalAreaColors = NormalAreaColors.copy( foreground = Grey14, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), selectionAreaColors = SelectionAreaColors.copy( foreground = Grey14, startBackground = Blue6, endBackground = Blue6, startBorderColor = Blue6, endBorderColor = Blue6, ), focusAreaColors = FocusAreaColors.copy( foreground = Grey14, startBackground = Blue6, endBackground = Blue6, startBorderColor = Color(0xFF101012), endBorderColor = Color(0xFF101012), ), disabledAreaColors = DisabledAreaColors.copy( foreground = Grey5, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ) ) val PrimaryButtonColors = ButtonColors( normalAreaColors = NormalAreaColors.copy( text = Grey14, startBackground = Blue6, endBackground = Blue6, startBorderColor = Blue6, endBorderColor = Blue6, ), focusAreaColors = FocusAreaColors.copy( text = Grey14, startBackground = Blue6, endBackground = Blue6, startBorderColor = Grey1, endBorderColor = Grey1, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey5, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ) ) val OutlineButtonColors = ButtonColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey5, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ) ) val LinkColors = LinkColors( normalAreaColors = NormalAreaColors.copy( text = Blue8, ), hoverAreaColors = HoverAreaColors.copy( text = Blue8, ), pressedAreaColors = PressedAreaColors.copy( text = Blue8, ), focusAreaColors = FocusAreaColors.copy( text = Blue8, ), disabledAreaColors = NormalAreaColors.copy( text = Grey5, ), visitedAreaColors = NormalAreaColors.copy( text = Blue4, ) ) val SegmentedButtonColors = SegmentedButtonColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ), itemNormalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, ), itemHoverAreaColors = HoverAreaColors.copy( text = Grey13, startBackground = Grey3, endBackground = Grey3, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, ), itemPressedAreaColors = PressedAreaColors.copy( text = Grey13, startBackground = Grey3, endBackground = Grey3, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, ), itemSelectionAreaColors = SelectionAreaColors.copy( text = Grey13, startBackground = Grey3, endBackground = Grey3, startBorderColor = Grey5, endBorderColor = Grey5, ), itemSelectedFocusAreaColors = SelectionAreaColors.copy( text = Grey13, startBackground = Blue3, endBackground = Blue3, startBorderColor = Grey3, endBorderColor = Grey3, ) ) val ToolTipColors = ToolTipColors( isDark = true, normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey3, endBackground = Grey3, startBorderColor = Grey4, endBorderColor = Grey4, ) ) val TextFieldColors = TextFieldColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey2, endBorderColor = Grey2, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey5, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ), errorAreaColors = ErrorAreaColors.copy( text = Red6, startBackground = Grey2, endBackground = Grey2, startBorderColor = Red6, endBorderColor = Red6, focusColor = Red6, ), errorFocusAreaColors = ErrorAreaColors.copy( text = Red6, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey2, endBorderColor = Grey2, focusColor = Red6, ) ) val DropdownMenuColors = DropdownMenuColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey4, endBorderColor = Grey4, ), hoverAreaColors = HoverAreaColors.copy( text = Grey13, startBackground = Blue2, endBackground = Blue2, startBorderColor = Blue2, endBorderColor = Blue2, ), pressedAreaColors = PressedAreaColors.copy( text = Grey13, startBackground = Blue2, endBackground = Blue2, startBorderColor = Blue2, endBorderColor = Blue2, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Blue2, endBackground = Blue2, startBorderColor = Blue2, endBorderColor = Blue2, ), ) val ComboBoxColors = ComboBoxColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey5, endBorderColor = Grey5, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey5, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey3, endBorderColor = Grey3, ), dropdownMenuColors = DropdownMenuColors ) val ContextMenuColors = ContextMenuColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey4, endBorderColor = Grey4, ), hoverAreaColors = HoverAreaColors.copy( text = Grey13, startBackground = Blue2, endBackground = Blue2, startBorderColor = Blue2, endBorderColor = Blue2, ), pressedAreaColors = PressedAreaColors.copy( text = Grey13, startBackground = Blue2, endBackground = Blue2, startBorderColor = Blue2, endBorderColor = Blue2, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Blue2, endBackground = Blue2, startBorderColor = Blue2, endBorderColor = Blue2, ), ) val ToolBarActionButtonColors = ToolBarActionButtonColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Grey4, endBackground = Grey4, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Grey4, endBackground = Grey4, ), disabledAreaColors = DisabledAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), selectionAreaColors = SelectionAreaColors.copy( foreground = Grey13, startBackground = Blue6, endBackground = Blue6, ), inactiveSelectionAreaColors = SelectionInactiveAreaColors.copy( startBackground = Grey4, endBackground = Grey4, ) ) val ProgressBarColors = ProgressBarColors( normalAreaColors = NormalAreaColors.copy( foreground = Blue7, startBackground = Grey4, endBackground = Grey4, ), indeterminateAreaColors = NormalAreaColors.copy( foreground = Blue9, startBackground = Blue9, endBackground = Blue5, ), ) val TabColors = TabColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Grey4, endBackground = Grey4, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Grey4, endBackground = Grey4, ), inactiveAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), selectionAreaColors = SelectionAreaColors.copy( focusColor = Blue7, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveSelectionAreaColors = SelectionInactiveAreaColors.copy( focusColor = Grey6, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ) ) val CloseableTabColors = TabColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), selectionAreaColors = SelectionAreaColors.copy( focusColor = Blue7, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveSelectionAreaColors = SelectionInactiveAreaColors.copy( focusColor = Grey6, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ) ) val DefaultTextStyle = TextStyle( fontFamily = Fonts.Inter, fontSize = 13.sp, fontWeight = FontWeight.Normal, color = Color.Unspecified ) val DefaultBoldTextStyle = DefaultTextStyle.copy(fontWeight = FontWeight.Bold) val ParagraphTextStyle = DefaultTextStyle.copy(fontSize = 13.sp, fontWeight = FontWeight.Normal, lineHeight = 19.sp) val MediumTextStyle = DefaultTextStyle.copy(fontSize = 12.sp, fontWeight = FontWeight.Normal, lineHeight = 15.sp) val MediumBoldTextStyle = MediumTextStyle.copy(fontWeight = FontWeight.Bold) val SmallTextStyle = DefaultTextStyle.copy(fontSize = 11.sp, fontWeight = FontWeight.Normal, lineHeight = 14.sp) val H0TextStyle = DefaultTextStyle.copy(fontSize = 25.sp, fontWeight = FontWeight.Medium) val H1TextStyle = DefaultTextStyle.copy(fontSize = 22.sp, fontWeight = FontWeight.Medium) val H2TextStyle = DefaultTextStyle.copy(fontSize = 18.sp, fontWeight = FontWeight.Normal) val H2BoldTextStyle = H2TextStyle.copy(fontWeight = FontWeight.Bold) val H3TextStyle = DefaultTextStyle.copy(fontSize = 16.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp) val H3BoldTextStyle = H3TextStyle.copy(fontWeight = FontWeight.Bold) override fun provideValues(): Array> { return arrayOf( LocalIsDarkTheme provides isDark, LocalAreaColors provides NormalAreaColors, LocalInactiveAreaColors provides InactiveAreaColors, LocalErrorAreaColors provides ErrorAreaColors, LocalErrorInactiveAreaColors provides ErrorInactiveAreaColors, LocalDisabledAreaColors provides DisabledAreaColors, LocalHoverAreaColors provides HoverAreaColors, LocalPressedAreaColors provides PressedAreaColors, LocalFocusAreaColors provides FocusAreaColors, LocalSelectionAreaColors provides SelectionAreaColors, LocalSelectionInactiveAreaColors provides SelectionInactiveAreaColors, LocalMainToolBarColors provides MainToolBarColors, LocalActionButtonColors provides ActionButtonColors, LocalCheckBoxColors provides CheckBoxColors, LocalRadioButtonColors provides RadioButtonColors, LocalPrimaryButtonColors provides PrimaryButtonColors, LocalOutlineButtonColors provides OutlineButtonColors, LocalLinkColors provides LinkColors, LocalSegmentedButtonColors provides SegmentedButtonColors, LocalToolTipColors provides ToolTipColors, LocalTextFieldColors provides TextFieldColors, LocalDropdownMenuColors provides DropdownMenuColors, LocalComboBoxColors provides ComboBoxColors, LocalContextMenuColors provides ContextMenuColors, LocalToolBarActionButtonColors provides ToolBarActionButtonColors, LocalProgressBarColors provides ProgressBarColors, LocalTabColors provides TabColors, LocalCloseableTabColors provides CloseableTabColors, LocalWindowsCloseWindowButtonColors provides WindowsCloseWindowButtonColors, LocalContextMenuRepresentation provides JbContextMenuRepresentation(ContextMenuColors), LocalDefaultTextStyle provides DefaultTextStyle, LocalDefaultBoldTextStyle provides DefaultBoldTextStyle, LocalParagraphTextStyle provides ParagraphTextStyle, LocalMediumTextStyle provides MediumTextStyle, LocalMediumBoldTextStyle provides MediumBoldTextStyle, LocalSmallTextStyle provides SmallTextStyle, LocalH0TextStyle provides H0TextStyle, LocalH1TextStyle provides H1TextStyle, LocalH2TextStyle provides H2TextStyle, LocalH2BoldTextStyle provides H2BoldTextStyle, LocalH3TextStyle provides H3TextStyle, LocalH3BoldTextStyle provides H3BoldTextStyle, ) } } @Composable fun DarkTheme(content: @Composable () -> Unit) { DarkTheme.provide(content) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/theme/Fonts.kt ================================================ package io.kanro.compose.jetbrains.expui.theme import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.platform.Font object Fonts { val InterRegular = Font(resource = "/fonts/Inter-Regular.ttf", weight = FontWeight.Normal) val InterBold = Font(resource = "/fonts/Inter-Bold.ttf", weight = FontWeight.Bold) val InterMedium = Font(resource = "/fonts/Inter-Medium.ttf", weight = FontWeight.Medium) val InterSemiBold = Font(resource = "/fonts/Inter-SemiBold.ttf", weight = FontWeight.SemiBold) val InterLight = Font(resource = "/fonts/Inter-Light.ttf", weight = FontWeight.Light) val InterThin = Font(resource = "/fonts/Inter-Thin.ttf", weight = FontWeight.Thin) val InterExtraLight = Font(resource = "/fonts/Inter-ExtraLight.ttf", weight = FontWeight.ExtraLight) val InterExtraBold = Font(resource = "/fonts/Inter-ExtraBold.ttf", weight = FontWeight.ExtraBold) val InterBlack = Font(resource = "/fonts/Inter-Black.ttf", weight = FontWeight.Black) val Inter = FontFamily( InterRegular, InterBold, InterMedium, InterSemiBold, InterLight, InterThin, InterExtraLight, InterExtraBold ) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/theme/LightTheme.kt ================================================ package io.kanro.compose.jetbrains.expui.theme import androidx.compose.foundation.LocalContextMenuRepresentation import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import io.kanro.compose.jetbrains.expui.control.ActionButtonColors import io.kanro.compose.jetbrains.expui.control.ButtonColors import io.kanro.compose.jetbrains.expui.control.CheckBoxColors import io.kanro.compose.jetbrains.expui.control.ComboBoxColors import io.kanro.compose.jetbrains.expui.control.ContextMenuColors import io.kanro.compose.jetbrains.expui.control.DropdownMenuColors import io.kanro.compose.jetbrains.expui.control.JbContextMenuRepresentation import io.kanro.compose.jetbrains.expui.control.LinkColors import io.kanro.compose.jetbrains.expui.control.LocalActionButtonColors import io.kanro.compose.jetbrains.expui.control.LocalCheckBoxColors import io.kanro.compose.jetbrains.expui.control.LocalCloseableTabColors import io.kanro.compose.jetbrains.expui.control.LocalComboBoxColors import io.kanro.compose.jetbrains.expui.control.LocalContextMenuColors import io.kanro.compose.jetbrains.expui.control.LocalDropdownMenuColors import io.kanro.compose.jetbrains.expui.control.LocalLinkColors import io.kanro.compose.jetbrains.expui.control.LocalOutlineButtonColors import io.kanro.compose.jetbrains.expui.control.LocalPrimaryButtonColors import io.kanro.compose.jetbrains.expui.control.LocalProgressBarColors import io.kanro.compose.jetbrains.expui.control.LocalRadioButtonColors import io.kanro.compose.jetbrains.expui.control.LocalSegmentedButtonColors import io.kanro.compose.jetbrains.expui.control.LocalTabColors import io.kanro.compose.jetbrains.expui.control.LocalTextFieldColors import io.kanro.compose.jetbrains.expui.control.LocalToolBarActionButtonColors import io.kanro.compose.jetbrains.expui.control.LocalToolTipColors import io.kanro.compose.jetbrains.expui.control.ProgressBarColors import io.kanro.compose.jetbrains.expui.control.RadioButtonColors import io.kanro.compose.jetbrains.expui.control.SegmentedButtonColors import io.kanro.compose.jetbrains.expui.control.TabColors import io.kanro.compose.jetbrains.expui.control.TextFieldColors import io.kanro.compose.jetbrains.expui.control.ToolBarActionButtonColors import io.kanro.compose.jetbrains.expui.control.ToolTipColors import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalDefaultBoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalDefaultTextStyle import io.kanro.compose.jetbrains.expui.style.LocalDisabledAreaColors import io.kanro.compose.jetbrains.expui.style.LocalErrorAreaColors import io.kanro.compose.jetbrains.expui.style.LocalErrorInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalFocusAreaColors import io.kanro.compose.jetbrains.expui.style.LocalH0TextStyle import io.kanro.compose.jetbrains.expui.style.LocalH1TextStyle import io.kanro.compose.jetbrains.expui.style.LocalH2BoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalH2TextStyle import io.kanro.compose.jetbrains.expui.style.LocalH3BoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalH3TextStyle import io.kanro.compose.jetbrains.expui.style.LocalHoverAreaColors import io.kanro.compose.jetbrains.expui.style.LocalInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalMediumBoldTextStyle import io.kanro.compose.jetbrains.expui.style.LocalMediumTextStyle import io.kanro.compose.jetbrains.expui.style.LocalParagraphTextStyle import io.kanro.compose.jetbrains.expui.style.LocalPressedAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSelectionInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalSmallTextStyle import io.kanro.compose.jetbrains.expui.window.LocalMainToolBarColors import io.kanro.compose.jetbrains.expui.window.LocalWindowsCloseWindowButtonColors import io.kanro.compose.jetbrains.expui.window.MainToolBarColors object LightTheme : Theme { val Grey1 = Color(0xFF000000) val Grey2 = Color(0xFF27282E) val Grey3 = Color(0xFF383A42) val Grey4 = Color(0xFF494B57) val Grey5 = Color(0xFF5A5D6B) val Grey6 = Color(0xFF6C707E) val Grey7 = Color(0xFF818594) val Grey8 = Color(0xFFA8ADBD) val Grey9 = Color(0xFFC9CCD6) val Grey10 = Color(0xFFDFE1E5) val Grey11 = Color(0xFFEBECF0) val Grey12 = Color(0xFFF7F8FA) val Grey13 = Color(0xFFFFFFFF) val Blue1 = Color(0xFF243D70) val Blue2 = Color(0xFF29498A) val Blue3 = Color(0xFF2E55A3) val Blue4 = Color(0xFF315FBD) val Blue5 = Color(0xFF3369D6) val Blue6 = Color(0xFF3573F0) val Blue7 = Color(0xFF407BF2) val Blue8 = Color(0xFF588CF3) val Blue9 = Color(0xFF709CF5) val Blue10 = Color(0xFF88ADF7) val Blue11 = Color(0xFFA0BDF8) val Blue12 = Color(0xFFB7CEFA) val Blue13 = Color(0xFFCFDEFC) val Blue14 = Color(0xFFE7EFFD) val Blue15 = Color(0xFFF5F8FE) val Green1 = Color(0xFF283829) val Green2 = Color(0xFF375239) val Green3 = Color(0xFF456B47) val Green4 = Color(0xFF508453) val Green5 = Color(0xFF599E5E) val Green6 = Color(0xFF5FB865) val Green7 = Color(0xFF7FC784) val Green8 = Color(0xFFA5D9A8) val Green9 = Color(0xFFC1E5C3) val Green10 = Color(0xFFDFF2E0) val Green11 = Color(0xFFF2FCF3) val Yellow1 = Color(0xFF7D5020) val Yellow2 = Color(0xFF96662A) val Yellow3 = Color(0xFFB07D35) val Yellow4 = Color(0xFFC99540) val Yellow5 = Color(0xFFE3AE4D) val Yellow6 = Color(0xFFFCC75B) val Yellow7 = Color(0xFFFACE70) val Yellow8 = Color(0xFFFCDB8D) val Yellow9 = Color(0xFFFFE7AB) val Yellow10 = Color(0xFFFFF0C7) val Yellow11 = Color(0xFFFFF8E3) val Red1 = Color(0xFF633333) val Red2 = Color(0xFF7D3C3C) val Red3 = Color(0xFF964444) val Red4 = Color(0xFFB04A4A) val Red5 = Color(0xFFC94F4F) val Red6 = Color(0xFFE35252) val Red7 = Color(0xFFEB7171) val Red8 = Color(0xFFF29191) val Red9 = Color(0xFFFAB4B4) val Red10 = Color(0xFFFCDBD4) val Red11 = Color(0xFFE8E8E8) val Red12 = Color(0xFFF5F5F5) val ErrorForeground = Color(0XFFD92B2B) val Orange1 = Color(0xFF7A4627) val Orange2 = Color(0xFF96572D) val Orange3 = Color(0xFFAD6531) val Orange4 = Color(0xFFC47233) val Orange5 = Color(0xFFDB8035) val Orange6 = Color(0xFFF28C35) val Orange7 = Color(0xFFFAA058) val Orange8 = Color(0xFFF7B47C) val Orange9 = Color(0xFFFACEAA) val Orange10 = Color(0xFFFCEDD4) val Orange11 = Color(0xFFFFF4EB) override val isDark: Boolean = false val NormalAreaColors = AreaColors( text = Grey1, foreground = Color.Unspecified, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey11, endBorderColor = Grey11, focusColor = Blue6, ) val InactiveAreaColors = NormalAreaColors.copy( text = Grey8 ) val ErrorAreaColors = NormalAreaColors.copy( text = ErrorForeground ) val ErrorInactiveAreaColors = ErrorAreaColors val DisabledAreaColors = NormalAreaColors.copy( text = Grey8, foreground = Grey8, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey10, endBorderColor = Grey10 ) val HoverAreaColors = NormalAreaColors.copy( startBackground = Blue14, endBackground = Blue14, ) val PressedAreaColors = NormalAreaColors.copy( startBackground = Grey10, endBackground = Grey10, ) val FocusAreaColors = NormalAreaColors.copy() val SelectionAreaColors = NormalAreaColors.copy( startBackground = Blue13, endBackground = Blue13, ) val SelectionInactiveAreaColors = NormalAreaColors.copy( startBackground = Grey10, endBackground = Grey10, ) val MainToolBarColors = MainToolBarColors( isDark = true, normalAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey2, endBackground = Grey2, startBorderColor = Grey1, endBorderColor = Grey1, focusColor = Blue6, ), inactiveAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey3, endBackground = Grey3, startBorderColor = Grey1, endBorderColor = Grey1, focusColor = Blue6, ), actionButtonColors = ActionButtonColors( normalAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), hoverAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey1, endBackground = Grey1, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), pressedAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Grey1, endBackground = Grey1, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), disabledAreaColors = AreaColors( text = Grey13, foreground = Grey6, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), ) ) val WindowsCloseWindowButtonColors = ActionButtonColors( normalAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), hoverAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Red6, endBackground = Red6, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Red6, ), pressedAreaColors = AreaColors( text = Grey13, foreground = Color.Unspecified, startBackground = Red6, endBackground = Red6, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Red6, ), disabledAreaColors = AreaColors( text = Grey13, foreground = Grey6, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, focusColor = Blue6, ), ) val ActionButtonColors = ActionButtonColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Grey11, endBackground = Grey11, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Grey10, endBackground = Grey10, ), disabledAreaColors = DisabledAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ) ) val CheckBoxColors = CheckBoxColors( normalAreaColors = NormalAreaColors.copy( foreground = Grey13, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), selectionAreaColors = SelectionAreaColors.copy( foreground = Grey13, startBackground = Blue6, endBackground = Blue6, startBorderColor = Blue6, endBorderColor = Blue6, ), focusAreaColors = FocusAreaColors.copy( foreground = Grey13, startBackground = Blue6, endBackground = Blue6, startBorderColor = Grey13, endBorderColor = Grey13, ), disabledAreaColors = DisabledAreaColors.copy( foreground = Grey8, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey10, endBorderColor = Grey10, ) ) val RadioButtonColors = RadioButtonColors( normalAreaColors = NormalAreaColors.copy( foreground = Grey13, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), selectionAreaColors = SelectionAreaColors.copy( foreground = Grey13, startBackground = Blue6, endBackground = Blue6, startBorderColor = Blue6, endBorderColor = Blue6, ), focusAreaColors = FocusAreaColors.copy( foreground = Grey13, startBackground = Blue6, endBackground = Blue6, startBorderColor = Grey13, endBorderColor = Grey13, ), disabledAreaColors = DisabledAreaColors.copy( foreground = Grey8, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey10, endBorderColor = Grey10, ) ) val PrimaryButtonColors = ButtonColors( normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Blue6, endBackground = Blue6, startBorderColor = Blue6, endBorderColor = Blue6, ), focusAreaColors = FocusAreaColors.copy( text = Grey13, startBackground = Blue6, endBackground = Blue6, startBorderColor = Grey13, endBorderColor = Grey13, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey8, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey10, endBorderColor = Grey10, ) ) val OutlineButtonColors = ButtonColors( normalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), focusAreaColors = FocusAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey13, endBorderColor = Grey13, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey8, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey10, endBorderColor = Grey10, ) ) val LinkColors = LinkColors( normalAreaColors = NormalAreaColors.copy( text = Blue4, ), hoverAreaColors = HoverAreaColors.copy( text = Blue4, ), pressedAreaColors = PressedAreaColors.copy( text = Blue4, ), focusAreaColors = FocusAreaColors.copy( text = Blue4, ), disabledAreaColors = NormalAreaColors.copy( text = Grey8, ), visitedAreaColors = NormalAreaColors.copy( text = Blue2, ) ) val SegmentedButtonColors = SegmentedButtonColors( normalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey8, endBorderColor = Grey8, ), focusAreaColors = FocusAreaColors.copy( text = Grey1, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey13, endBorderColor = Grey13, ), itemNormalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Color.Unspecified, endBackground = Color.Unspecified, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, ), itemHoverAreaColors = HoverAreaColors.copy( text = Grey1, startBackground = Grey11, endBackground = Grey11, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, ), itemPressedAreaColors = PressedAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Color.Unspecified, endBorderColor = Color.Unspecified, ), itemSelectionAreaColors = SelectionAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), itemSelectedFocusAreaColors = SelectionAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Grey13, endBorderColor = Grey13, ) ) val ToolTipColors = ToolTipColors( isDark = true, normalAreaColors = NormalAreaColors.copy( text = Grey13, startBackground = Grey1, endBackground = Grey1, startBorderColor = Grey1, endBorderColor = Grey1, ) ) val TextFieldColors = TextFieldColors( normalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), focusAreaColors = FocusAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey13, endBorderColor = Grey13, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey8, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey10, endBorderColor = Grey10, ), errorAreaColors = ErrorAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Red6, endBorderColor = Red6, focusColor = Red6, ), errorFocusAreaColors = ErrorAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey13, endBorderColor = Grey13, focusColor = Red6, ) ) val DropdownMenuColors = DropdownMenuColors( normalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), hoverAreaColors = HoverAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Blue13, endBorderColor = Blue13, ), pressedAreaColors = PressedAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Blue13, endBorderColor = Blue13, ), focusAreaColors = FocusAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Blue13, endBorderColor = Blue13, ), ) val ComboBoxColors = ComboBoxColors( normalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), focusAreaColors = FocusAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey13, endBorderColor = Grey13, ), disabledAreaColors = DisabledAreaColors.copy( text = Grey8, startBackground = Grey12, endBackground = Grey12, startBorderColor = Grey10, endBorderColor = Grey10, ), dropdownMenuColors = DropdownMenuColors ) val ContextMenuColors = ContextMenuColors( normalAreaColors = NormalAreaColors.copy( text = Grey1, startBackground = Grey13, endBackground = Grey13, startBorderColor = Grey8, endBorderColor = Grey8, ), hoverAreaColors = HoverAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Blue13, endBorderColor = Blue13, ), pressedAreaColors = PressedAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Blue13, endBorderColor = Blue13, ), focusAreaColors = FocusAreaColors.copy( text = Grey1, startBackground = Blue13, endBackground = Blue13, startBorderColor = Blue13, endBorderColor = Blue13, ), ) val ToolBarActionButtonColors = ToolBarActionButtonColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Grey10, endBackground = Grey10, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Grey10, endBackground = Grey10, ), disabledAreaColors = DisabledAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), selectionAreaColors = SelectionAreaColors.copy( foreground = Grey13, startBackground = Blue6, endBackground = Blue6, ), inactiveSelectionAreaColors = SelectionInactiveAreaColors.copy( startBackground = Grey10, endBackground = Grey10, ) ) val ProgressBarColors = ProgressBarColors( normalAreaColors = NormalAreaColors.copy( foreground = Blue6, startBackground = Grey10, endBackground = Grey10, ), indeterminateAreaColors = NormalAreaColors.copy( foreground = Blue6, startBackground = Blue6, endBackground = Blue11, ), ) val TabColors = TabColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Grey11, endBackground = Grey11, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Grey11, endBackground = Grey11, ), inactiveAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), selectionAreaColors = SelectionAreaColors.copy( focusColor = Blue8, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveSelectionAreaColors = SelectionInactiveAreaColors.copy( focusColor = Grey8, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ) ) val CloseableTabColors = TabColors( normalAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), hoverAreaColors = HoverAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), pressedAreaColors = PressedAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveAreaColors = NormalAreaColors.copy( startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), selectionAreaColors = SelectionAreaColors.copy( focusColor = Blue8, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ), inactiveSelectionAreaColors = SelectionInactiveAreaColors.copy( focusColor = Grey8, startBackground = Color.Unspecified, endBackground = Color.Unspecified, ) ) val DefaultTextStyle = TextStyle( fontFamily = Fonts.Inter, fontSize = 13.sp, fontWeight = FontWeight.Normal, color = Color.Unspecified ) val DefaultBoldTextStyle = DefaultTextStyle.copy(fontWeight = FontWeight.Bold) val ParagraphTextStyle = DefaultTextStyle.copy(fontSize = 13.sp, fontWeight = FontWeight.Normal, lineHeight = 19.sp) val MediumTextStyle = DefaultTextStyle.copy(fontSize = 12.sp, fontWeight = FontWeight.Normal, lineHeight = 15.sp) val MediumBoldTextStyle = MediumTextStyle.copy(fontWeight = FontWeight.Bold) val SmallTextStyle = DefaultTextStyle.copy(fontSize = 11.sp, fontWeight = FontWeight.Normal, lineHeight = 14.sp) val H0TextStyle = DefaultTextStyle.copy(fontSize = 25.sp, fontWeight = FontWeight.Medium) val H1TextStyle = DefaultTextStyle.copy(fontSize = 22.sp, fontWeight = FontWeight.Medium) val H2TextStyle = DefaultTextStyle.copy(fontSize = 18.sp, fontWeight = FontWeight.Normal) val H2BoldTextStyle = H2TextStyle.copy(fontWeight = FontWeight.Bold) val H3TextStyle = DefaultTextStyle.copy(fontSize = 16.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp) val H3BoldTextStyle = H3TextStyle.copy(fontWeight = FontWeight.Bold) override fun provideValues(): Array> { return arrayOf( LocalIsDarkTheme provides isDark, LocalAreaColors provides NormalAreaColors, LocalInactiveAreaColors provides InactiveAreaColors, LocalErrorAreaColors provides ErrorAreaColors, LocalErrorInactiveAreaColors provides ErrorInactiveAreaColors, LocalDisabledAreaColors provides DisabledAreaColors, LocalHoverAreaColors provides HoverAreaColors, LocalPressedAreaColors provides PressedAreaColors, LocalFocusAreaColors provides FocusAreaColors, LocalSelectionAreaColors provides SelectionAreaColors, LocalSelectionInactiveAreaColors provides SelectionInactiveAreaColors, LocalMainToolBarColors provides MainToolBarColors, LocalActionButtonColors provides ActionButtonColors, LocalCheckBoxColors provides CheckBoxColors, LocalRadioButtonColors provides RadioButtonColors, LocalPrimaryButtonColors provides PrimaryButtonColors, LocalOutlineButtonColors provides OutlineButtonColors, LocalLinkColors provides LinkColors, LocalSegmentedButtonColors provides SegmentedButtonColors, LocalToolTipColors provides ToolTipColors, LocalTextFieldColors provides TextFieldColors, LocalDropdownMenuColors provides DropdownMenuColors, LocalComboBoxColors provides ComboBoxColors, LocalContextMenuColors provides ContextMenuColors, LocalToolBarActionButtonColors provides ToolBarActionButtonColors, LocalProgressBarColors provides ProgressBarColors, LocalTabColors provides TabColors, LocalCloseableTabColors provides CloseableTabColors, LocalWindowsCloseWindowButtonColors provides WindowsCloseWindowButtonColors, LocalContextMenuRepresentation provides JbContextMenuRepresentation(ContextMenuColors), LocalDefaultTextStyle provides DefaultTextStyle, LocalDefaultBoldTextStyle provides DefaultBoldTextStyle, LocalParagraphTextStyle provides ParagraphTextStyle, LocalMediumTextStyle provides MediumTextStyle, LocalMediumBoldTextStyle provides MediumBoldTextStyle, LocalSmallTextStyle provides SmallTextStyle, LocalH0TextStyle provides H0TextStyle, LocalH1TextStyle provides H1TextStyle, LocalH2TextStyle provides H2TextStyle, LocalH2BoldTextStyle provides H2BoldTextStyle, LocalH3TextStyle provides H3TextStyle, LocalH3BoldTextStyle provides H3BoldTextStyle, ) } } @Composable fun LightTheme(content: @Composable () -> Unit) { LightTheme.provide(content) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/theme/Theme.kt ================================================ package io.kanro.compose.jetbrains.expui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidedValue import androidx.compose.runtime.compositionLocalOf val LocalIsDarkTheme = compositionLocalOf { false } interface Theme { val isDark: Boolean @Composable fun provide(content: @Composable () -> Unit) { CompositionLocalProvider( *provideValues(), content = content, ) } fun provideValues(): Array> } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/util/CustomWindowDecorationAccessing.kt ================================================ package io.kanro.compose.jetbrains.expui.util import java.awt.Shape import java.awt.Window import java.lang.reflect.Method internal object CustomWindowDecorationAccessing { init { UnsafeAccessing.assignAccessibility( UnsafeAccessing.desktopModule, listOf( "java.awt" ) ) } private val customWindowDecorationInstance: Any? = try { val customWindowDecoration = Class.forName("java.awt.Window\$CustomWindowDecoration") val constructor = customWindowDecoration.declaredConstructors.first() constructor.isAccessible = true constructor.newInstance() } catch (e: Exception) { null } private val setCustomDecorationEnabledMethod: Method? = getMethod("setCustomDecorationEnabled", Window::class.java, Boolean::class.java) private val setCustomDecorationTitleBarHeightMethod: Method? = getMethod("setCustomDecorationTitleBarHeight", Window::class.java, Int::class.java) private val setCustomDecorationHitTestSpotsMethod: Method? = getMethod("setCustomDecorationHitTestSpots", Window::class.java, MutableList::class.java) private fun getMethod(name: String, vararg params: Class<*>): Method? { return try { val clazz = Class.forName("java.awt.Window\$CustomWindowDecoration") val method = clazz.getDeclaredMethod( name, *params ) method.isAccessible = true method } catch (e: Exception) { null } } fun setCustomDecorationEnabled(window: Window, enabled: Boolean) { val instance = customWindowDecorationInstance ?: return val method = setCustomDecorationEnabledMethod ?: return method.invoke(instance, window, enabled) } fun setCustomDecorationTitleBarHeight(window: Window, height: Int) { val instance = customWindowDecorationInstance ?: return val method = setCustomDecorationTitleBarHeightMethod ?: return method.invoke(instance, window, height) } fun setCustomDecorationHitTestSpotsMethod(window: Window, spots: Map) { val instance = customWindowDecorationInstance ?: return val method = setCustomDecorationHitTestSpotsMethod ?: return method.invoke(instance, window, spots.entries.toMutableList()) } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/util/UnsafeAccessing.kt ================================================ package io.kanro.compose.jetbrains.expui.util import sun.misc.Unsafe import java.lang.reflect.AccessibleObject internal object UnsafeAccessing { private val unsafe: Any? by lazy { try { val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") theUnsafe.isAccessible = true theUnsafe.get(null) as Unsafe } catch (e: Throwable) { null } } val desktopModule by lazy { ModuleLayer.boot().findModule("java.desktop").get() } val ownerModule by lazy { this.javaClass.module } private val isAccessibleFieldOffset: Long? by lazy { try { (unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first")) } catch (e: Throwable) { null } } private val implAddOpens by lazy { try { Module::class.java.getDeclaredMethod( "implAddOpens", String::class.java, Module::class.java ).accessible() } catch (e: Throwable) { null } } fun assignAccessibility(obj: AccessibleObject) { try { val theUnsafe = unsafe as? Unsafe ?: return val offset = isAccessibleFieldOffset ?: return theUnsafe.putBooleanVolatile(obj, offset, true) } catch (e: Throwable) { // ignore } } fun assignAccessibility(module: Module, packages: List) { try { packages.forEach { implAddOpens?.invoke(module, it, ownerModule) } } catch (e: Throwable) { // ignore } } private class Parent { var first = false @Volatile var second: Any? = null } } internal fun T.accessible(): T { return apply { UnsafeAccessing.assignAccessibility(this) } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.Linux.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.rememberWindowState import io.kanro.compose.jetbrains.expui.control.LocalContentActivated import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.theme.Theme @Composable internal fun JBWindowOnLinux( onCloseRequest: () -> Unit, state: WindowState = rememberWindowState(), visible: Boolean = true, title: String = "", theme: Theme = LightTheme, resizable: Boolean = true, enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, mainToolBar: (@Composable MainToolBarScope.() -> Unit)?, content: @Composable FrameWindowScope.() -> Unit, ) { Window( onCloseRequest, state, visible, title, null, false, false, resizable, enabled, focusable, alwaysOnTop, onPreviewKeyEvent, onKeyEvent ) { CompositionLocalProvider( LocalWindow provides window, LocalContentActivated provides LocalWindowInfo.current.isWindowFocused, *theme.provideValues() ) { Column(Modifier.fillMaxSize()) { MainToolBarOnLinux(content = mainToolBar) Spacer(Modifier.fillMaxWidth().height(1.dp).background(LocalAreaColors.current.startBorderColor)) Box(Modifier.fillMaxSize().areaBackground()) { content() } } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.MacOS.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.rememberWindowState import io.kanro.compose.jetbrains.expui.control.LocalContentActivated import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.theme.Theme import java.awt.event.ComponentEvent import java.awt.event.ComponentListener @Composable internal fun JBWindowOnMacOS( onCloseRequest: () -> Unit, state: WindowState = rememberWindowState(), visible: Boolean = true, title: String = "", showTitle: Boolean = true, theme: Theme = LightTheme, resizable: Boolean = true, enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, mainToolBar: (@Composable MainToolBarScope.() -> Unit)?, content: @Composable FrameWindowScope.() -> Unit, ) { Window( onCloseRequest, state, visible, title, null, false, false, resizable, enabled, focusable, alwaysOnTop, onPreviewKeyEvent, onKeyEvent ) { LaunchedEffect(Unit, theme) { val rootPane = window.rootPane rootPane.putClientProperty( "apple.awt.windowAppearance", if (theme.isDark) "NSAppearanceNameVibrantDark" else "NSAppearanceNameVibrantLight" ) } CompositionLocalProvider( LocalWindow provides window, LocalContentActivated provides LocalWindowInfo.current.isWindowFocused, *theme.provideValues() ) { Column(Modifier.fillMaxSize()) { val isFullscreen by rememberWindowIsFullscreen() MainToolBarOnMacOS(title, showTitle, isFullscreen, content = mainToolBar) Spacer(Modifier.fillMaxWidth().height(1.dp).background(LocalAreaColors.current.startBorderColor)) Box(Modifier.fillMaxSize().areaBackground()) { content() } } } } } @Composable fun FrameWindowScope.rememberWindowIsFullscreen(): State { val isFullscreen = remember { mutableStateOf(window.placement == WindowPlacement.Fullscreen) } DisposableEffect(window) { val listener = object : ComponentListener { override fun componentResized(e: ComponentEvent?) { isFullscreen.value = window.placement == WindowPlacement.Fullscreen } override fun componentMoved(e: ComponentEvent?) {} override fun componentShown(e: ComponentEvent?) {} override fun componentHidden(e: ComponentEvent?) {} } window.addComponentListener(listener) onDispose { window.removeComponentListener(listener) } } return isFullscreen } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.Windows.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.rememberWindowState import io.kanro.compose.jetbrains.expui.control.LocalContentActivated import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.areaBackground import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.theme.Theme @Composable internal fun JBWindowOnWindows( onCloseRequest: () -> Unit, state: WindowState = rememberWindowState(), visible: Boolean = true, title: String = "", showTitle: Boolean = true, theme: Theme = LightTheme, icon: Painter? = null, resizable: Boolean = true, enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, mainToolBar: (@Composable MainToolBarScope.() -> Unit)?, content: @Composable FrameWindowScope.() -> Unit, ) { Window( onCloseRequest, state, visible, title, icon, false, false, resizable, enabled, focusable, alwaysOnTop, onPreviewKeyEvent, onKeyEvent ) { CompositionLocalProvider( LocalWindow provides window, LocalContentActivated provides LocalWindowInfo.current.isWindowFocused, *theme.provideValues() ) { Column(Modifier.fillMaxSize()) { MainToolBarOnWindows(icon, state, onCloseRequest, title, showTitle, resizable, content = mainToolBar) Spacer(Modifier.fillMaxWidth().height(1.dp).background(LocalAreaColors.current.startBorderColor)) Box(Modifier.fillMaxSize().areaBackground()) { content() } } } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/JBWindow.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.res.painterResource import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.rememberWindowState import io.kanro.compose.jetbrains.expui.DesktopPlatform import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.theme.Theme import javax.swing.JFrame val LocalWindow = compositionLocalOf { error("CompositionLocal LocalWindow not provided") } @Composable fun JBWindow( onCloseRequest: () -> Unit, state: WindowState = rememberWindowState(), visible: Boolean = true, title: String = "", showTitle: Boolean = true, theme: Theme = LightTheme, icon: Painter? = painterResource("icons/compose.svg"), resizable: Boolean = true, enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, mainToolBar: (@Composable MainToolBarScope.() -> Unit)? = null, content: @Composable FrameWindowScope.() -> Unit, ) { when (DesktopPlatform.Current) { DesktopPlatform.Linux -> JBWindowOnLinux( onCloseRequest, state, visible, title, theme, resizable, enabled, focusable, alwaysOnTop, onPreviewKeyEvent, onKeyEvent, mainToolBar, content ) DesktopPlatform.Windows -> JBWindowOnWindows( onCloseRequest, state, visible, title, showTitle, theme, icon, resizable, enabled, focusable, alwaysOnTop, onPreviewKeyEvent, onKeyEvent, mainToolBar, content ) DesktopPlatform.MacOS -> JBWindowOnMacOS( onCloseRequest, state, visible, title, showTitle, theme, resizable, enabled, focusable, alwaysOnTop, onPreviewKeyEvent, onKeyEvent, mainToolBar, content ) DesktopPlatform.Unknown -> TODO() } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.Basic.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope import io.kanro.compose.jetbrains.expui.control.LocalContentActivated import io.kanro.compose.jetbrains.expui.style.areaBackground @Composable fun FrameWindowScope.BasicMainToolBar( colors: MainToolBarColors = LocalMainToolBarColors.current, content: (@Composable MainToolBarScope.() -> Unit)?, ) { colors.provideArea(LocalContentActivated.current) { Layout( content = { with(MainToolBarScopeInstance) { content?.invoke(this) } }, modifier = Modifier.fillMaxWidth().height(40.dp).areaBackground(), measurePolicy = rememberMainToolBarMeasurePolicy(window) ) } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.Linux.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.runtime.Composable import androidx.compose.ui.window.FrameWindowScope @Composable internal fun FrameWindowScope.MainToolBarOnLinux( colors: MainToolBarColors = LocalMainToolBarColors.current, content: (@Composable MainToolBarScope.() -> Unit)?, ) { BasicMainToolBar(colors, content) } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.MacOS.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope @Composable internal fun FrameWindowScope.MainToolBarOnMacOS( title: String, showTitle: Boolean, isFullScreen: Boolean, colors: MainToolBarColors = LocalMainToolBarColors.current, content: (@Composable MainToolBarScope.() -> Unit)?, ) { BasicMainToolBar(colors) { if (isFullScreen) { Spacer(Modifier.width(80.dp).mainToolBarItem(Alignment.Start, true)) } if (showTitle) { MainToolBarTitle(title) } content?.invoke(this) } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.Windows.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowState import io.kanro.compose.jetbrains.expui.control.ActionButton import io.kanro.compose.jetbrains.expui.control.Icon import io.kanro.compose.jetbrains.expui.control.LocalActionButtonColors import io.kanro.compose.jetbrains.expui.control.LocalContentActivated import io.kanro.compose.jetbrains.expui.theme.LightTheme @Composable internal fun FrameWindowScope.MainToolBarOnWindows( icon: Painter?, windowState: WindowState, onCloseRequest: () -> Unit, title: String, showTitle: Boolean, resizeable: Boolean, colors: MainToolBarColors = LocalMainToolBarColors.current, content: (@Composable MainToolBarScope.() -> Unit)?, ) { BasicMainToolBar(colors) { if (icon != null) { Box( modifier = Modifier.size(40.dp).mainToolBarItem(Alignment.Start, true), contentAlignment = Alignment.Center ) { Icon(icon) } } WindowsSystemButtons(windowState, resizeable, onCloseRequest) if (showTitle) { MainToolBarTitle(title) } content?.invoke(this) } } val LocalWindowsCloseWindowButtonColors = compositionLocalOf { LightTheme.WindowsCloseWindowButtonColors } @Composable private fun MainToolBarScope.WindowsSystemButtons( windowState: WindowState, resizeable: Boolean, onCloseRequest: () -> Unit, ) { val active = LocalContentActivated.current CompositionLocalProvider( LocalActionButtonColors provides LocalWindowsCloseWindowButtonColors.current ) { ActionButton( { onCloseRequest() }, Modifier.focusProperties { canFocus = false }.size(40.dp).mainToolBarItem(Alignment.End), shape = RectangleShape ) { if (active) { Icon("icons/windows/closeActive.svg") } else { Icon("icons/windows/closeInactive.svg") } } } ActionButton( { windowState.placement = when (windowState.placement) { WindowPlacement.Floating -> WindowPlacement.Maximized WindowPlacement.Maximized -> WindowPlacement.Floating WindowPlacement.Fullscreen -> WindowPlacement.Fullscreen } }, Modifier.focusProperties { canFocus = false }.size(40.dp).mainToolBarItem(Alignment.End), enabled = resizeable, shape = RectangleShape ) { if (windowState.placement == WindowPlacement.Floating) { if (active) { Icon("icons/windows/maximize.svg") } else { Icon("icons/windows/maximizeInactive.svg") } } else { if (active) { Icon("icons/windows/restore.svg") } else { Icon("icons/windows/restoreInactive.svg") } } } ActionButton( { windowState.isMinimized = true }, Modifier.focusProperties { canFocus = false }.size(40.dp).mainToolBarItem(Alignment.End), shape = RectangleShape ) { if (active) { Icon("icons/windows/minimize.svg") } else { Icon("icons/windows/minimizeInactive.svg") } } } ================================================ FILE: expui/src/main/kotlin/io/kanro/compose/jetbrains/expui/window/MainToolBar.kt ================================================ package io.kanro.compose.jetbrains.expui.window import androidx.compose.foundation.layout.LayoutScopeMarker import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.InspectorValueInfo import androidx.compose.ui.platform.NoInspectorInfo import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.offset import io.kanro.compose.jetbrains.expui.control.ActionButtonColors import io.kanro.compose.jetbrains.expui.control.Label import io.kanro.compose.jetbrains.expui.control.LocalActionButtonColors import io.kanro.compose.jetbrains.expui.style.AreaColors import io.kanro.compose.jetbrains.expui.style.AreaProvider import io.kanro.compose.jetbrains.expui.style.InactiveAreaProvider import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalInactiveAreaColors import io.kanro.compose.jetbrains.expui.style.LocalNormalAreaColors import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.theme.LocalIsDarkTheme import io.kanro.compose.jetbrains.expui.util.CustomWindowDecorationAccessing import java.awt.Rectangle import java.awt.Shape import java.awt.Window import kotlin.math.max data class MainToolBarColors( val isDark: Boolean, override val normalAreaColors: AreaColors, override val inactiveAreaColors: AreaColors, val actionButtonColors: ActionButtonColors, ) : AreaProvider, InactiveAreaProvider { @Composable fun provideArea(isActive: Boolean, content: @Composable () -> Unit) { val currentColors = if (isActive) normalAreaColors else inactiveAreaColors CompositionLocalProvider( LocalAreaColors provides currentColors, LocalNormalAreaColors provides normalAreaColors, LocalInactiveAreaColors provides inactiveAreaColors, LocalActionButtonColors provides actionButtonColors, LocalIsDarkTheme provides isDark, content = content ) } } val LocalMainToolBarColors = compositionLocalOf { LightTheme.MainToolBarColors } @LayoutScopeMarker @Immutable interface MainToolBarScope { @Stable fun Modifier.mainToolBarItem( alignment: Alignment.Horizontal, draggableArea: Boolean = false, ): Modifier } internal object MainToolBarScopeInstance : MainToolBarScope { override fun Modifier.mainToolBarItem(alignment: Alignment.Horizontal, draggableArea: Boolean): Modifier { return this.then( MainToolBarChildData( horizontalAlignment = alignment, draggableArea = draggableArea, inspectorInfo = debugInspectorInfo { name = "mainToolBarItem" properties["alignment"] = alignment properties["draggableArea"] = draggableArea } ) ) } } internal class MainToolBarChildData( var horizontalAlignment: Alignment.Horizontal, var draggableArea: Boolean, inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo, ) : ParentDataModifier, InspectorValueInfo(inspectorInfo) { override fun Density.modifyParentData(parentData: Any?): Any { return this@MainToolBarChildData } fun spotRule(): Int { return if (draggableArea) 0 else 1 } } internal class MainToolBarMeasurePolicy(private val window: Window) : MeasurePolicy { override fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult { if (measurables.isEmpty()) { return layout( constraints.minWidth, constraints.minHeight ) { CustomWindowDecorationAccessing.setCustomDecorationEnabled(window, true) CustomWindowDecorationAccessing.setCustomDecorationTitleBarHeight( window, constraints.minHeight.toDp().value.toInt() ) } } var occupiedSpaceHorizontally = 0 var maxSpaceVertically = constraints.minHeight val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0) val measuredPlaceable = mutableListOf>() for (it in measurables) { val placeable = it.measure(contentConstraints.offset(horizontal = -occupiedSpaceHorizontally)) if (constraints.maxWidth < occupiedSpaceHorizontally + placeable.width) { break } occupiedSpaceHorizontally += placeable.width maxSpaceVertically = max(maxSpaceVertically, placeable.height) measuredPlaceable += it to placeable } val boxWidth = maxOf(constraints.minWidth, occupiedSpaceHorizontally) val boxHeight = maxSpaceVertically return layout(boxWidth, boxHeight) { val placeableGroups = measuredPlaceable.groupBy { (measurable, _) -> (measurable.parentData as? MainToolBarChildData)?.horizontalAlignment ?: Alignment.CenterHorizontally } val spots = mutableMapOf() var headUsedSpace = 0 var trailerUsedSpace = 0 placeableGroups[Alignment.Start]?.forEach { (measurable, placeable) -> val spotRule = (measurable.parentData as? MainToolBarChildData)?.spotRule() ?: 1 val x = headUsedSpace val y = Alignment.CenterVertically.align(placeable.height, boxHeight) placeable.placeRelative(x, y) headUsedSpace += placeable.width spots[PxToDpRectangle(x, y, placeable.width, placeable.height)] = spotRule } placeableGroups[Alignment.End]?.forEach { (measurable, placeable) -> val spotRule = (measurable.parentData as? MainToolBarChildData)?.spotRule() ?: 1 val x = boxWidth - placeable.width - trailerUsedSpace val y = Alignment.CenterVertically.align(placeable.height, boxHeight) placeable.placeRelative(x, y) trailerUsedSpace += placeable.width spots[PxToDpRectangle(x, y, placeable.width, placeable.height)] = spotRule } val centerPlaceable = placeableGroups[Alignment.CenterHorizontally] ?: listOf() val requiredCenterSpace = centerPlaceable.sumOf { it.second.width } val minX = headUsedSpace val maxX = boxWidth - trailerUsedSpace - requiredCenterSpace var centerX = (boxWidth - requiredCenterSpace) / 2 if (minX <= maxX) { if (centerX > maxX) { centerX = maxX } if (centerX < minX) { centerX = minX } centerPlaceable.forEach { (measurable, placeable) -> val spotRule = (measurable.parentData as? MainToolBarChildData)?.spotRule() ?: 1 val x = centerX val y = Alignment.CenterVertically.align(placeable.height, boxHeight) placeable.placeRelative(x, y) centerX += placeable.width spots[PxToDpRectangle(x, y, placeable.width, placeable.height)] = spotRule } } CustomWindowDecorationAccessing.setCustomDecorationEnabled(window, true) CustomWindowDecorationAccessing.setCustomDecorationTitleBarHeight(window, boxHeight.toDp().value.toInt()) CustomWindowDecorationAccessing.setCustomDecorationHitTestSpotsMethod(window, spots) } } } internal fun Density.PxToDpRectangle(x: Int, y: Int, width: Int, height: Int): Rectangle { return Rectangle( x.toDp().value.toInt(), y.toDp().value.toInt(), width.toDp().value.toInt(), height.toDp().value.toInt() ) } @Composable internal fun rememberMainToolBarMeasurePolicy(window: Window): MeasurePolicy { return remember(window) { MainToolBarMeasurePolicy(window) } } @Composable fun MainToolBarScope.MainToolBarTitle(title: String) { Label(title, Modifier.mainToolBarItem(Alignment.CenterHorizontally, true), maxLines = 1) } ================================================ FILE: expui-gallery/build.gradle.kts ================================================ plugins { kotlin("jvm") id("org.jetbrains.compose") } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation(project(":compose-jetbrains-expui-theme")) implementation(compose.desktop.currentOs) { exclude("org.jetbrains.compose.material") } implementation(compose.uiTooling) } compose.desktop { application { mainClass = "MainKt" nativeDistributions { packageName = "JetBrains ExpUI Gallery" packageVersion = project.version.toString() copyright = "Beijing Muke Technology Co., Ltd." modules("jdk.unsupported") } } } ================================================ FILE: expui-gallery/src/main/kotlin/Main.kt ================================================ import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectableGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import io.kanro.compose.jetbrains.expui.control.ActionButton import io.kanro.compose.jetbrains.expui.control.CloseableTab import io.kanro.compose.jetbrains.expui.control.ComboBox import io.kanro.compose.jetbrains.expui.control.DropdownLink import io.kanro.compose.jetbrains.expui.control.DropdownMenu import io.kanro.compose.jetbrains.expui.control.DropdownMenuItem import io.kanro.compose.jetbrains.expui.control.ExternalLink import io.kanro.compose.jetbrains.expui.control.Icon import io.kanro.compose.jetbrains.expui.control.Label import io.kanro.compose.jetbrains.expui.control.Link import io.kanro.compose.jetbrains.expui.control.OutlineButton import io.kanro.compose.jetbrains.expui.control.PrimaryButton import io.kanro.compose.jetbrains.expui.control.ProgressBar import io.kanro.compose.jetbrains.expui.control.RadioButton import io.kanro.compose.jetbrains.expui.control.SegmentedButton import io.kanro.compose.jetbrains.expui.control.Tab import io.kanro.compose.jetbrains.expui.control.TextArea import io.kanro.compose.jetbrains.expui.control.TextField import io.kanro.compose.jetbrains.expui.control.ToolBarActionButton import io.kanro.compose.jetbrains.expui.control.Tooltip import io.kanro.compose.jetbrains.expui.control.TriStateCheckbox import io.kanro.compose.jetbrains.expui.style.LocalAreaColors import io.kanro.compose.jetbrains.expui.style.LocalErrorAreaColors import io.kanro.compose.jetbrains.expui.theme.DarkTheme import io.kanro.compose.jetbrains.expui.theme.LightTheme import io.kanro.compose.jetbrains.expui.window.JBWindow import java.awt.Desktop import java.net.URI import kotlin.system.exitProcess @OptIn(ExperimentalFoundationApi::class) fun main() { application { var isDark by remember { mutableStateOf(false) } val theme = if (isDark) { DarkTheme } else { LightTheme } JBWindow( title = "JetBrains ExpUI Gallery", theme = theme, state = rememberWindowState(size = DpSize(900.dp, 700.dp)), onCloseRequest = { exitApplication() exitProcess(0) }, mainToolBar = { Row(Modifier.mainToolBarItem(Alignment.End)) { Tooltip("Open GitHub link in browser") { ActionButton( { Desktop.getDesktop() .browse(URI.create("https://github.com/ButterCam/compose-jetbrains-theme")) }, Modifier.size(40.dp), shape = RectangleShape ) { Icon("icons/github.svg") } } Tooltip("Switch between dark and light mode,\ncurrently is ${if (isDark) "dark" else "light"} mode") { ActionButton( { isDark = !isDark }, Modifier.size(40.dp), shape = RectangleShape ) { if (isDark) { Icon("icons/darkTheme.svg") } else { Icon("icons/lightTheme.svg") } } } } }) { Row( Modifier.fillMaxSize() ) { Column( Modifier.fillMaxHeight().width(40.dp).padding(vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally ) { var selected by remember { mutableStateOf(0) } ToolBarActionButton( selected == 0, { selected = 0 }, modifier = Modifier.size(30.dp) ) { Icon("icons/generic.svg", markerColor = LocalErrorAreaColors.current.text) } ToolBarActionButton( selected == 1, { selected = 1 }, modifier = Modifier.size(30.dp) ) { Icon("icons/text.svg") } } Spacer(Modifier.background(LocalAreaColors.current.startBorderColor).width(1.dp).fillMaxHeight()) Column( Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally ) { Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { var state by remember { mutableStateOf(ToggleableState.Indeterminate) } Tooltip("An action button") { ActionButton({}, Modifier.size(30.dp)) { Icon("icons/settings.svg") } } Tooltip("An interactive tri-state checkbox") { TriStateCheckbox(state, { state = when (state) { ToggleableState.Off -> ToggleableState.On ToggleableState.On -> ToggleableState.Off ToggleableState.Indeterminate -> ToggleableState.On } }) } Tooltip("A non-interactive tri-state checkbox, always is off") { TriStateCheckbox(ToggleableState.Off, {}) { Label("Off") } } Tooltip("A non-interactive tri-state checkbox, always is on") { TriStateCheckbox(ToggleableState.On, {}) { Label("On") } } Tooltip("A disabled tri-state checkbox, always is indeterminate") { TriStateCheckbox(ToggleableState.Indeterminate, {}, enabled = false) { Label("Indeterminate") } } Tooltip("A disabled tri-state checkbox, always is off") { TriStateCheckbox(ToggleableState.Off, {}, enabled = false) { Label("Disabled") } } Tooltip("A disabled tri-state checkbox, always is on") { TriStateCheckbox(ToggleableState.On, {}, enabled = false) { Label("Disabled") } } } Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { Tooltip("An outline style button") { OutlineButton({}, modifier = Modifier.width(90.dp)) { Label("Outline") } } Tooltip("An disabled button") { PrimaryButton({}, modifier = Modifier.width(90.dp), enabled = false) { Label("Disabled") } } Tooltip("A primary style button") { PrimaryButton({}, modifier = Modifier.width(90.dp)) { Label("Primary") } } } Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { Tooltip("Just some text") { Label("Label") } Tooltip("A clickable text") { Link("Link", {}) } Tooltip("It can not be clicked anymore") { Link("Disabled Link", {}, enabled = false) } Tooltip("Link to external website") { ExternalLink("External Link", {}) } Tooltip("Link that can open a dropdown list") { var menuOpen by remember { mutableStateOf(false) } DropdownLink("Dropdown Link", { menuOpen = true }) DropdownMenu(menuOpen, { menuOpen = false }) { DropdownMenuItem({ menuOpen = false }) { Label("Item 1") } DropdownMenuItem({ menuOpen = false }) { Label("Item 2") } DropdownMenuItem({ menuOpen = false }) { Label("Item 3") } } } } Tooltip("A segmented button, or radio button group") { var selectedIndex by remember { mutableStateOf(0) } SegmentedButton(3, selectedIndex, { selectedIndex = it }) { when (it) { 0 -> Label("First") 1 -> Label("Second") 2 -> Label("Third") else -> Label("Unknown") } } } Row( modifier = Modifier.selectableGroup(), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { var selectedIndex by remember { mutableStateOf(0) } RadioButton(selectedIndex == 0, { selectedIndex = 0 }) { Label("First") } RadioButton(selectedIndex == 1, { selectedIndex = 1 }, enabled = false) { Label("Second") } RadioButton(selectedIndex == 2, { selectedIndex = 2 }) { Label("Third") } RadioButton(selectedIndex == 3, { selectedIndex = 3 }) { Label("Fourth") } } TextField("TextField", {}) TextField("Rect", {}, shape = RectangleShape) TextArea("This is a text area\nIt can be multiline", {}) val comboBoxItems = remember { (0..100).map { "Item $it" } } var comboBoxSelection by remember { mutableStateOf(comboBoxItems.first()) } ComboBox(comboBoxItems, comboBoxSelection, { comboBoxSelection = it }, modifier = Modifier.width(150.dp), menuModifier = Modifier.width(150.dp)) InfiniteProgressBar() ProgressBar() Row(Modifier.height(40.dp).selectableGroup()) { var selected by remember { mutableStateOf(0) } Tab(selected == 0, { selected = 0 }, modifier = Modifier.fillMaxHeight()) { Label("First") } Tab(selected == 1, { selected = 1 }, modifier = Modifier.fillMaxHeight()) { Label("Second") } Tab(selected == 2, { selected = 2 }, modifier = Modifier.fillMaxHeight()) { Label("Third") } } Row(Modifier.height(40.dp).selectableGroup()) { var selected by remember { mutableStateOf(0) } CloseableTab(selected == 0, { selected = 0 }, {}, modifier = Modifier.fillMaxHeight()) { Icon("icons/kotlin.svg") Label("First.kt") } CloseableTab(selected == 1, { selected = 1 }, {}, modifier = Modifier.fillMaxHeight()) { Icon("icons/kotlin.svg") Label("Second.kt") } CloseableTab(selected == 2, { selected = 2 }, {}, modifier = Modifier.fillMaxHeight()) { Icon("icons/kotlin.svg") Label("Third.kt") } } } } } } } @Composable fun InfiniteProgressBar() { val transition = rememberInfiniteTransition() val currentOffset by transition.animateFloat(0f, 1f, infiniteRepeatable(animation = keyframes { durationMillis = 1000 })) ProgressBar(currentOffset) } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ kotlin.code.style=official ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MSYS* | MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { mavenLocal() gradlePluginPortal() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } include("classic") include("expui") include("expui-gallery") project(":classic").name = "compose-jetbrains-theme" project(":expui").name = "compose-jetbrains-expui-theme" project(":expui-gallery").name = "compose-jetbrains-expui-gallery"