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).

## 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

#### Gradle JVM settings

### `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
## Windows


## Linux(Ubuntu with Gnome)


================================================
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/).

## 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"