Initial commit

GitOrigin-RevId: e4b68ae8dc2075cd6efe46842e20567b05577083
This commit is contained in:
Sebastiano Poggi
2022-02-15 11:49:29 +01:00
committed by intellij-monorepo-bot
commit a32066d516
159 changed files with 7967 additions and 0 deletions

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

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

View File

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

View File

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

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

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

After

Width:  |  Height:  |  Size: 427 B

View File

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

View File

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

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

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

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

@@ -0,0 +1,49 @@
# Jewel: a Compose for Desktop theme
<img alt="Jewel logo" src="art/jewel-logo.svg" width="20%"/>
Jewel aims at recreating the _Darcula_ Swing Look and Feel used on the IntelliJ Platform into Compose for Desktop. For historical reasons, there is
another theme, Toolbox, which is derived from the JetBrains Toolbox codebase. The themes share some concepts and the general structure, but not much
in terms of code. All shared code is extracted to a separate module, `library`.
## Project structure
The project is split in modules:
1. `library` is the base Jewel library code (utils, interfaces, etc.)
2. `sample` is a stand-alone sample app of the Jewel themes
3. `themes` are the two themes implemented by Jewel:
1. `intellij` is the Darcula theme, which has two implementations:
1. `standalone` is the base theme and can be used in any Compose for Desktop project
2. `idea` is a version of the theme that can be used in an IDEA plugin, and integrates with the IDE's Swing LaF and themes via a bridge (more
on that later).
2. `toolbox` is the JetBrains Toolbox theme implementation. There is only a stand-alone implementation of this theme.
### Running the samples
To run the stand-alone sample app, you can run the `:sample:run` Gradle task.
To run the IntelliJ IDEA plugin sample, you can run the `:themes:intellij:idea:runIde` Gradle task. This will download and run a copy of IJ Community
with the plugin installed; you can check the JewelDemo panel in the IDE once it starts up (it's at the bottom, by default).
If you're in an IDE, you can use the "Stand-alone sample" and "IDE sample" run configurations.
### The Swing Bridge
In the `idea` module, there is a crucial element for proper integration with the IDE: a bridge between the Swing theme and LaF, and the Compose world.
This bridge ensures that we pick up the colours, typography, metrics, and images as defined in the current IntelliJ theme, and apply them to the
Compose theme as well.
The work of building this bridge is fairly complex as there isn't a good mapping between the IDE LaF properties, the Darcula design specs, and the
Compose implementations. Sometimes, you will need to get a bit creative.
When adding a new composable to the IJ theme, you need to make sure you also update the bridge to properly support it at runtime. You can refer to the
[Darcula design specs](https://jetbrains.design/intellij) and corresponding [Figma specs](https://jetbrains.design/intellij/resources/UI_kit/), but
the ultimate goal is consistency with the Swing implementation, so the ground truth of what you see in the IDE is the reference for any implementation
and trumps the specs.
To find the required values in the IDE, we recommend enabling
the [IDE internal mode](https://plugins.jetbrains.com/docs/intellij/enabling-internal.html)
and using the [UI Inspector](https://plugins.jetbrains.com/docs/intellij/internal-ui-inspector.html) and
[LaF Defaults](https://plugins.jetbrains.com/docs/intellij/internal-ui-laf-defaults.html) tools to figure out the names of the parameters to use in
the bridge.

View File

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

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.composeDesktop) apply false
alias(libs.plugins.ideaGradlePlugin) apply false
alias(libs.plugins.kotlinSerialization) apply false
}
allprojects {
group = "org.jetbrains.jewel"
version = "0.1-SNAPSHOT"
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}

View File

@@ -0,0 +1,20 @@
[versions]
composeDesktop = "1.1.0-alpha03"
coroutines = "1.5.2"
ideaGradlePlugin = "1.3.0"
jna = "5.10.0"
kotlin = "1.6.10"
kotlinxSerialization = "1.3.1"
[libraries]
jna = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
[plugins]
composeDesktop = { id = "org.jetbrains.compose", version.ref = "composeDesktop" }
ideaGradlePlugin = { id = "org.jetbrains.intellij", version.ref = "ideaGradlePlugin" }
kotlinJs = { id = "org.jetbrains.kotlin.js", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
platform/jewel/gradlew vendored Normal file
View File

@@ -0,0 +1,185 @@
#!/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" "$@"

89
platform/jewel/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@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

View File

@@ -0,0 +1,40 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeDesktop)
alias(libs.plugins.kotlinSerialization)
`maven-publish`
}
kotlin {
target {
compilations.all {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
}
}
dependencies {
compileOnly(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
implementation(libs.kotlinx.serialization.json)
implementation(libs.jna)
}
val sourcesJar by tasks.creating(Jar::class) {
from(kotlin.sourceSets.main.get().kotlin)
archiveClassifier.set("source")
}
publishing {
publications {
create<MavenPublication>("main") {
from(components["kotlin"])
artifact(sourcesJar)
artifactId = rootProject.name
}
}
}

View File

@@ -0,0 +1,50 @@
package org.jetbrains.jewel
import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Immutable
data class Insets(
@Stable
val left: Dp,
@Stable
val top: Dp,
@Stable
val right: Dp,
@Stable
val bottom: Dp
) {
constructor(all: Dp) : this(all, all, all, all)
constructor(horizontal: Dp, vertical: Dp) : this(horizontal, vertical, horizontal, vertical)
companion object {
val Empty = Insets(0.dp)
}
}
val InsetsVectorConverter = TwoWayConverter<Insets, AnimationVector4D>(
convertToVector = { AnimationVector4D(it.left.value, it.top.value, it.right.value, it.bottom.value) },
convertFromVector = { Insets(it.v1.dp, it.v2.dp, it.v3.dp, it.v4.dp) }
)
@Composable
inline fun <S> Transition<S>.animateInsets(
noinline transitionSpec:
@Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Insets> = { spring() },
label: String = "InsetsAnimation",
targetValueByState: @Composable() (state: S) -> Insets
): State<Insets> {
return animateValue(InsetsVectorConverter, transitionSpec, label, targetValueByState)
}

View File

@@ -0,0 +1,18 @@
package org.jetbrains.jewel
import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationInstance
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
object NoIndication : Indication {
private object NoIndicationInstance : IndicationInstance {
override fun ContentDrawScope.drawIndication() = drawContent()
}
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance =
NoIndicationInstance
}

View File

@@ -0,0 +1,6 @@
package org.jetbrains.jewel
enum class Orientation {
Horizontal,
Vertical,
}

View File

@@ -0,0 +1,9 @@
package org.jetbrains.jewel
private val osName = System.getProperty("os.name")
fun isMacOs(): Boolean = osName.startsWith("mac", ignoreCase = true)
fun isWindows(): Boolean = osName.startsWith("windows", ignoreCase = true)
fun isLinux(): Boolean = osName.startsWith("linux", ignoreCase = true)

View File

@@ -0,0 +1,169 @@
package org.jetbrains.jewel
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSimple
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Dp
fun Modifier.shape(shape: Shape, shapeStroke: ShapeStroke? = null, fillColor: Color = Color.Unspecified): Modifier =
shape(shape, shapeStroke, fillColor.nullIfUnspecified()?.toBrush())
fun Modifier.shape(shape: Shape, shapeStroke: ShapeStroke? = null, fillBrush: Brush?): Modifier =
composed(
factory = {
this.then(
when {
shape === RectangleShape -> rectangleModifier(shapeStroke, fillBrush)
else -> shapeModifier(shapeStroke, fillBrush, shape)
}
)
},
inspectorInfo = debugInspectorInfo {
name = "shape"
properties["stroke"] = shapeStroke
properties["shape"] = shape
}
)
private fun rectangleModifier(shapeStroke: ShapeStroke?, brush: Brush?) = Modifier.drawWithCache {
if (shapeStroke != null) {
val strokeWidth = if (shapeStroke.width == Dp.Hairline) 1f else shapeStroke.width.toPx()
val stroke = Stroke(strokeWidth)
val insets = shapeStroke.insets
val insetOffset = Offset(insets.left.toPx(), insets.top.toPx())
val insetSize = Size(
size.width - insets.left.toPx() - insets.right.toPx(),
size.height - insets.top.toPx() - insets.bottom.toPx()
)
drawRectangleShape(insetOffset, insetSize, stroke, shapeStroke.brush, brush)
} else {
drawRectangleShape(Offset.Zero, size, null, null, brush)
}
}
private fun CacheDrawScope.drawRectangleShape(
insetOffset: Offset,
insetSize: Size,
stroke: Stroke?,
strokeBrush: Brush?,
fillBrush: Brush?
) =
onDrawWithContent {
val strokeWidth = stroke?.width ?: 0f
val enoughSpace = size.width > strokeWidth && size.height > strokeWidth
if (fillBrush != null && enoughSpace) {
drawRect(brush = fillBrush, topLeft = insetOffset, size = insetSize, style = Fill)
}
drawContent()
if (stroke != null && strokeBrush != null && enoughSpace)
drawRect(brush = strokeBrush, topLeft = insetOffset, size = insetSize, style = stroke)
}
private fun CacheDrawScope.drawRoundedShape(
insetOffset: Offset,
outline: Outline.Rounded,
stroke: Stroke?,
strokeBrush: Brush?,
fillBrush: Brush?
) =
onDrawWithContent {
when {
outline.roundRect.isSimple -> {
val roundRect = outline.roundRect
if (fillBrush != null) {
withTransform({ translate(insetOffset.x, insetOffset.y) }) {
drawRoundRect(
brush = fillBrush,
topLeft = Offset(roundRect.left, roundRect.top),
size = Size(roundRect.width, roundRect.height),
cornerRadius = roundRect.topLeftCornerRadius,
style = Fill
)
}
}
drawContent()
if (stroke != null && strokeBrush != null)
withTransform({ translate(insetOffset.x, insetOffset.y) }) {
drawRoundRect(
brush = strokeBrush,
topLeft = Offset(roundRect.left, roundRect.top),
size = Size(roundRect.width, roundRect.height),
cornerRadius = roundRect.topLeftCornerRadius,
style = stroke
)
}
}
else -> {
val path = Path().apply {
addRoundRect(outline.roundRect)
translate(insetOffset)
}
if (fillBrush != null) {
drawPath(path, brush = fillBrush, style = Fill)
}
drawContent()
if (stroke != null && strokeBrush != null)
drawPath(path, strokeBrush, style = stroke)
}
}
}
private fun CacheDrawScope.drawPathShape(path: Path, stroke: Stroke?, strokeBrush: Brush?, fillBrush: Brush?) =
onDrawWithContent {
if (fillBrush != null) {
drawPath(path, brush = fillBrush, style = Fill)
}
drawContent()
if (stroke != null && strokeBrush != null)
drawPath(path, strokeBrush, style = stroke)
}
private fun shapeModifier(shapeStroke: ShapeStroke?, fillBrush: Brush?, shape: Shape) = Modifier.drawWithCache {
val strokeWidth = when (shapeStroke?.width) {
null -> 0f
Dp.Hairline -> 1f
else -> shapeStroke.width.toPx()
}
val insets = shapeStroke?.insets ?: Insets.Empty
val insetOffset = Offset(insets.left.toPx(), insets.top.toPx())
val insetSize = Size(
size.width - insets.left.toPx() - insets.right.toPx(),
size.height - insets.top.toPx() - insets.bottom.toPx()
)
val stroke = if (shapeStroke != null) Stroke(strokeWidth) else null
val strokeBrush = shapeStroke?.brush
val outline: Outline = shape.createOutline(insetSize, layoutDirection, this)
when {
size.minDimension > 0f -> when (outline) {
is Outline.Rectangle -> drawRectangleShape(insetOffset, insetSize, stroke, strokeBrush, fillBrush)
is Outline.Rounded -> drawRoundedShape(insetOffset, outline, stroke, strokeBrush, fillBrush)
is Outline.Generic -> {
val path = Path().apply { addPath(outline.path, insetOffset) }
drawPathShape(path, stroke, strokeBrush, fillBrush)
}
}
else -> onDrawWithContent {
drawContent()
}
}
}
fun Color.toBrush() = SolidColor(this)
private fun Color.nullIfUnspecified() = takeIf { it != Color.Unspecified }

View File

@@ -0,0 +1,30 @@
package org.jetbrains.jewel
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateDp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Immutable
data class ShapeStroke(val width: Dp, val brush: Brush, val insets: Insets = Insets(width / 2))
@Composable
inline fun <S> Transition<S>.animateShapeStroke(
label: String = "ShapeStrokeAnimation",
targetValueByState: @Composable (state: S) -> ShapeStroke?
): State<ShapeStroke> {
val width by animateDp(label = "$label.width") { targetValueByState(it)?.width ?: 0.dp }
// TODO val color by animateColor(label = "$label.color") { targetValueByState(it)?.color ?: Color.Unspecified }
val insets by animateInsets(label = "$label.insets") { targetValueByState(it)?.insets ?: Insets.Empty }
return derivedStateOf { ShapeStroke(width, SolidColor(Color.Red), insets) }
}

View File

@@ -0,0 +1,31 @@
package org.jetbrains.jewel
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@Stable
val BottomLineShape: Shape = object : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density) =
Outline.Generic(Path().apply {
moveTo(0f, size.height)
lineTo(size.width, size.height)
})
override fun toString(): String = "BottomLineShape"
}
@Stable
val RightLineShape: Shape = object : Shape {
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density) =
Outline.Generic(Path().apply {
moveTo(size.width, 0f)
lineTo(size.width, size.height)
})
override fun toString(): String = "RightLineShape"
}

View File

@@ -0,0 +1,259 @@
package org.jetbrains.jewel.components
import androidx.compose.runtime.Immutable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
import kotlin.math.roundToInt
fun imageSlices(all: Int) = ImageSliceValues(all)
fun imageSlices(horizontal: Int, vertical: Int) = ImageSliceValues(horizontal, vertical)
fun imageSlices(left: Int, top: Int, right: Int, bottom: Int) = ImageSliceValues(left, top, right, bottom)
// todo: think about RTL?
@Immutable
data class ImageSliceValues(
val left: Int = 0,
val top: Int = 0,
val right: Int = 0,
val bottom: Int = 0,
) {
constructor(all: Int) : this(all, all, all, all)
constructor(horizontal: Int, vertical: Int) : this(horizontal, vertical, horizontal, vertical)
val horizontal get() = left + right
val vertical get() = top + bottom
}
@Immutable
data class ImageSlice(val image: ImageBitmap, val slices: ImageSliceValues) {
fun draw(
scope: DrawScope,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
val area = IntSize(scope.size.width.roundToInt(), scope.size.height.roundToInt())
val hMiddleSize = image.width - slices.horizontal
val vMiddleSize = image.height - slices.vertical
val hTimes = (area.width - slices.horizontal) / hMiddleSize
val vTimes = (area.height - slices.vertical) / vMiddleSize
val hExtra = area.width - slices.horizontal - hTimes * hMiddleSize
val vExtra = area.height - slices.vertical - vTimes * vMiddleSize
// top row
scope.drawSlice(
image,
srcOffset = IntOffset.Zero,
srcSize = IntSize(slices.left, slices.top),
dstOffset = IntOffset.Zero,
alpha = alpha, colorFilter = colorFilter
)
repeat(hTimes) { h ->
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, 0),
srcSize = IntSize(hMiddleSize, slices.top),
dstOffset = IntOffset(slices.left + h * hMiddleSize, 0),
alpha = alpha, colorFilter = colorFilter
)
}
if (hExtra > 0) {
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, 0),
srcSize = IntSize(hExtra, slices.top),
dstOffset = IntOffset(area.width - slices.right - hExtra, 0),
alpha = alpha, colorFilter = colorFilter
)
}
scope.drawSlice(
image,
srcOffset = IntOffset(image.width - slices.right, 0),
srcSize = IntSize(slices.right, slices.top),
dstOffset = IntOffset(area.width - slices.right, 0),
alpha = alpha, colorFilter = colorFilter
)
// left and right
repeat(vTimes) { v ->
scope.drawSlice(
image,
srcOffset = IntOffset(0, slices.top),
srcSize = IntSize(slices.left, vMiddleSize),
dstOffset = IntOffset(0, slices.top + v * vMiddleSize),
alpha = alpha, colorFilter = colorFilter
)
scope.drawSlice(
image,
srcOffset = IntOffset(image.width - slices.right, slices.top),
srcSize = IntSize(slices.right, vMiddleSize),
dstOffset = IntOffset(area.width - slices.right, slices.top + v * vMiddleSize),
alpha = alpha, colorFilter = colorFilter
)
}
if (vExtra > 0) {
scope.drawSlice(
image,
srcOffset = IntOffset(0, slices.top),
srcSize = IntSize(slices.left, vExtra),
dstOffset = IntOffset(0, area.height - slices.bottom - vExtra),
alpha = alpha, colorFilter = colorFilter
)
scope.drawSlice(
image,
srcOffset = IntOffset(image.width - slices.right, slices.top),
srcSize = IntSize(slices.right, vExtra),
dstOffset = IntOffset(area.width - slices.right, area.height - slices.bottom - vExtra),
alpha = alpha, colorFilter = colorFilter
)
}
// filler
repeat(vTimes) { v ->
repeat(hTimes) { h ->
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, slices.top),
srcSize = IntSize(hMiddleSize, vMiddleSize),
dstOffset = IntOffset(slices.left + h * hMiddleSize, slices.top + v * vMiddleSize),
alpha = alpha, colorFilter = colorFilter
)
}
if (hExtra > 0) {
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, slices.top),
srcSize = IntSize(hExtra, vMiddleSize),
dstOffset = IntOffset(area.width - slices.right - hExtra, slices.top + v * vMiddleSize),
alpha = alpha, colorFilter = colorFilter
)
}
}
if (vExtra > 0) {
repeat(hTimes) { h ->
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, slices.top),
srcSize = IntSize(hMiddleSize, vExtra),
dstOffset = IntOffset(slices.left + h * hMiddleSize, area.height - slices.bottom - vExtra),
alpha = alpha, colorFilter = colorFilter
)
}
if (hExtra > 0) {
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, slices.top),
srcSize = IntSize(hExtra, vExtra),
dstOffset = IntOffset(
area.width - slices.right - hExtra,
area.height - slices.bottom - vExtra
),
alpha = alpha, colorFilter = colorFilter
)
}
}
// bottom row
scope.drawSlice(
image,
srcOffset = IntOffset(0, image.height - slices.bottom),
srcSize = IntSize(slices.left, slices.bottom),
dstOffset = IntOffset(0, area.height - slices.bottom),
alpha = alpha, colorFilter = colorFilter
)
repeat(hTimes) {
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, image.height - slices.bottom),
srcSize = IntSize(hMiddleSize, slices.bottom),
dstOffset = IntOffset(slices.left + it * hMiddleSize, area.height - slices.bottom),
alpha = alpha, colorFilter = colorFilter
)
}
if (hExtra > 0) {
scope.drawSlice(
image,
srcOffset = IntOffset(slices.left, image.height - slices.bottom),
srcSize = IntSize(hExtra, slices.bottom),
dstOffset = IntOffset(area.width - slices.right - hExtra, area.height - slices.bottom),
alpha = alpha, colorFilter = colorFilter
)
}
scope.drawSlice(
image,
srcOffset = IntOffset(image.width - slices.right, image.height - slices.bottom),
srcSize = IntSize(slices.right, slices.bottom),
dstOffset = IntOffset(area.width - slices.right, area.height - slices.bottom),
alpha = alpha, colorFilter = colorFilter
)
}
}
data class ImageSlicePainter(
private val imageSlice: ImageSlice,
private val scale: Float
) : Painter() {
init {
validateSize(imageSlice.slices)
}
private var alpha: Float = 1.0f
private var colorFilter: ColorFilter? = null
override fun DrawScope.onDraw() {
imageSlice.draw(this, alpha, colorFilter)
}
/**
* Return the dimension of the underlying [ImageBitmap] as it's intrinsic width and height
*/
override val intrinsicSize: Size get() = IntSize(imageSlice.image.width, imageSlice.image.height).toSize()
override fun applyAlpha(alpha: Float): Boolean {
this.alpha = alpha
return true
}
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
this.colorFilter = colorFilter
return true
}
private fun validateSize(slices: ImageSliceValues) {
require(
slices.top >= 0 &&
slices.bottom >= 0 &&
slices.left >= 0 &&
slices.right >= 0 &&
slices.horizontal <= imageSlice.image.width &&
slices.vertical <= imageSlice.image.height
)
}
}
private fun DrawScope.drawSlice(
bitmap: ImageBitmap,
srcOffset: IntOffset,
srcSize: IntSize,
dstOffset: IntOffset,
dstSize: IntSize = srcSize,
alpha: Float,
colorFilter: ColorFilter?
) {
drawImage(bitmap, srcOffset, srcSize, dstOffset, dstSize, alpha = alpha, colorFilter = colorFilter)
}

View File

@@ -0,0 +1,29 @@
package org.jetbrains.jewel.components.state
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Brush
import org.jetbrains.jewel.ShapeStroke
enum class ButtonMouseState {
None,
Hovered,
Pressed
}
data class ButtonState(
val mouse: ButtonMouseState = ButtonMouseState.None,
val enabled: Boolean = true,
val focused: Boolean = false,
)
class AppearanceTransitionState(
background: State<Brush?>,
shapeStroke: State<ShapeStroke?>,
haloStroke: State<ShapeStroke?>,
) {
val background by background
val shapeStroke by shapeStroke
val haloStroke by haloStroke
}

View File

@@ -0,0 +1,10 @@
package org.jetbrains.jewel.components.state
import androidx.compose.ui.state.ToggleableState
data class CheckboxState(
val toggle: ToggleableState,
val mouse: ButtonMouseState = ButtonMouseState.None,
val enabled: Boolean = true,
val focused: Boolean = false,
)

View File

@@ -0,0 +1,8 @@
package org.jetbrains.jewel.components.state
enum class TabState {
Normal,
Selected,
Hovered,
Disabled,
}

View File

@@ -0,0 +1,13 @@
package org.jetbrains.jewel.components.state
data class TextFieldState(
val enabled: Boolean = true,
val hovered: Boolean = false,
val focused: Boolean = false,
) {
companion object {
val Default = TextFieldState()
}
}

View File

@@ -0,0 +1,25 @@
package org.jetbrains.jewel.font
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.io.File
fun Flow<File>.asFileProviderFlow(origin: FileProvider.Origin) =
map { FileProvider(it.name, it.extension, it.absolutePath, origin) { it } }
data class FileProvider(
val name: String,
val extension: String,
val path: String,
val origin: Origin,
val provider: () -> File
) {
enum class Origin {
SYSTEM_API,
FILESYSTEM,
CLASSPATH,
RESOURCES,
OTHER
}
}

View File

@@ -0,0 +1,165 @@
package org.jetbrains.jewel.font
import com.sun.jna.platform.win32.Advapi32Util
import com.sun.jna.platform.win32.WinReg
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.jetbrains.jewel.isLinux
import org.jetbrains.jewel.isMacOs
import org.jetbrains.jewel.isWindows
import java.io.File
import java.util.TreeMap
import java.util.zip.ZipFile
import kotlin.io.path.createTempFile
import kotlin.io.path.inputStream
import kotlin.io.path.readLines
// Note: TTC (TrueType Collection) support in AWT is pretty abysmal — it will load them, but
// only the first entry in the ttc file will ever be available.
val supportedFontFileExtensions = listOf("ttf", "otf", "ttc")
@OptIn(FlowPreview::class)
private val DEFAULT_LINUX_FONTS
get() = flowOf("/usr/share/fonts", "/usr/local/share/fonts", "${System.getProperty("user.home")}/.fonts")
.map { File(it) }
.flatMapMerge { it.walkTopDown().asFlow() }
.filter { supportedFontFileExtensions.contains(it.extension.lowercase()) }
.asFileProviderFlow(FileProvider.Origin.FILESYSTEM)
@OptIn(FlowPreview::class)
private val DEFAULT_MACOS_FONTS
get() = flowOf("/Library/Fonts", "/System/Library/Fonts")
.map { File(it) }
.flatMapMerge { it.walkTopDown().asFlow() }
.filter { supportedFontFileExtensions.contains(it.extension.lowercase()) }
.asFileProviderFlow(FileProvider.Origin.FILESYSTEM)
private val DEFAULT_WINDOWS_FONTS
get() = File(" C:\\Windows\\Fonts")
.walkTopDown()
.asFlow()
.filter { supportedFontFileExtensions.contains(it.extension.lowercase()) }
.asFileProviderFlow(FileProvider.Origin.FILESYSTEM)
@OptIn(ExperimentalCoroutinesApi::class)
fun getAvailableFontFiles(): Flow<FileProvider> {
val osSpecificFonts = when {
isLinux() -> merge(DEFAULT_LINUX_FONTS, getLinuxFontsUsingFcList())
isWindows() -> merge(DEFAULT_WINDOWS_FONTS, getWindowsFontsUsingRegistry())
isMacOs() -> merge(DEFAULT_MACOS_FONTS, getMacOSFontsUsingSystemProfiler())
else -> error("Unsupported OS: ${System.getProperty("os.name")}")
}
return merge(osSpecificFonts, getClasspathFonts())
}
private const val WINDOWS_FONTS_KEY_PATH = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"
// Current limitations:
// * If a font has a different "real" family name (as reported by AWT) from the name it appears with
// in the registry, that font will not be matched, and thus won't be listed
// * Font substitutions and "system" fonts (like Monospaced, SansSerif, etc) aren't listed — but the
// former are available as FontFamily.Monospaced, FontFamily.SansSerif, etc at least
private fun getWindowsFontsUsingRegistry(): Flow<FileProvider> {
@Suppress("UNCHECKED_CAST")
val registryMap = (Advapi32Util.registryGetValues(WinReg.HKEY_LOCAL_MACHINE, WINDOWS_FONTS_KEY_PATH) as TreeMap<String, String>)
val fontsDir = File("${System.getenv("WINDIR")}\\Fonts")
// AWT doesn't know how to handle ttc files correctly — it only ever loads the first font in a ttc.
// So, when we find a ttc entry with more than one font defined, we just get the first entry, hoping
// that the order is the same as inside the ttc. Not that we have any control over this anyway!
return registryMap.values.asFlow()
.map { if (it.contains('\\')) File(it) else File(fontsDir, it) }
.filter { it.exists() && supportedFontFileExtensions.contains(it.extension.lowercase()) }
.asFileProviderFlow(FileProvider.Origin.SYSTEM_API)
}
private fun getLinuxFontsUsingFcList(): Flow<FileProvider> {
val file = createTempFile()
ProcessBuilder("fc-list")
.redirectOutput(file.toFile())
.start()
.waitFor()
return file.readLines()
.asFlow()
.map { File(it) }
.filter { it.exists() && supportedFontFileExtensions.contains(it.extension.lowercase()) }
.asFileProviderFlow(FileProvider.Origin.SYSTEM_API)
}
private val json = Json { ignoreUnknownKeys = true }
@OptIn(ExperimentalSerializationApi::class)
private fun getMacOSFontsUsingSystemProfiler(): Flow<FileProvider> {
val file = createTempFile()
ProcessBuilder("system_profiler", "-json", "SPFontsDataType")
.redirectOutput(file.toFile())
.start()
.waitFor()
val fontListingOutput = file.inputStream()
.use { json.decodeFromStream<MacOsSystemProfilerFontListingOutput>(it) }
return fontListingOutput.fontData.asFlow()
.mapNotNull { fontData -> File(fontData.path).takeIf { it.exists() } }
.asFileProviderFlow(FileProvider.Origin.SYSTEM_API)
}
/**
* Scans the classpath for supported font files.
*
* @return A flow with all font files found.
* @see supportedFontFileExtensions
*/
@OptIn(FlowPreview::class)
fun getClasspathFonts() =
System.getProperty("java.class.path", ".")
.split(System.getProperty("path.separator").toRegex())
.asFlow()
.map { File(it) }
.flatMapMerge {
if (it.isDirectory) {
it.walkTopDown().asFlow().asFileProviderFlow(FileProvider.Origin.CLASSPATH)
} else {
zipFileFlow(it)
}
}
.filter { it.extension.lowercase() in supportedFontFileExtensions }
private fun zipFileFlow(file: File) = flow {
val zip = withContext(Dispatchers.IO) { ZipFile(file) }
zip.entries().asSequence().forEach { zipEntry ->
val name = zipEntry.name.substringBeforeLast(".")
val extension = zipEntry.name.substringAfterLast(".")
val path = "${file.absolutePath}${File.separator}$name.$extension"
val fileProvider = FileProvider(name, extension, path, FileProvider.Origin.CLASSPATH) {
val tmpFile = createTempFile().toFile()
tmpFile.outputStream().use { output ->
zip.getInputStream(zipEntry).use { input ->
input.transferTo(output)
}
}
tmpFile
}
emit(fileProvider)
}
withContext(Dispatchers.IO) { zip.close() }
}

View File

@@ -0,0 +1,120 @@
package org.jetbrains.jewel.font
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.platform.FileFont
import androidx.compose.ui.text.platform.Font
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import java.awt.GraphicsEnvironment
import java.awt.font.TextAttribute
import java.io.File
import java.util.Locale
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import java.awt.Font as AwtFont
import java.awt.Font.createFont as createAwtFont
object FontsLoader {
suspend fun loadFontsFrom(fontFileProviders: List<FileProvider>): Map<String, SystemFontFamily> {
return collectIntoSystemFontFamilies(
fontFamilyNames = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getAvailableFontFamilyNames(Locale.ROOT)
.toList(),
fontFiles = fontFileProviders.parallelMap(Dispatchers.IO) { it.provider() }
.parallelMap(Dispatchers.IO) { runCatching { createAwtFont(AwtFont.TRUETYPE_FONT, it) }.getOrNull() to it }
.mapNotNull { (key, value) -> key?.let { it to value } }
.toMap()
)
}
private suspend fun <T, R> Iterable<T>.parallelMap(
context: CoroutineContext = EmptyCoroutineContext,
transform: suspend (T) -> R
) =
if (context != EmptyCoroutineContext) {
withContext(context) { map { async { transform(it) } }.awaitAll() }
} else {
coroutineScope { map { async { transform(it) } }.awaitAll() }
}
private fun collectIntoSystemFontFamilies(
fontFamilyNames: Iterable<String>,
fontFiles: Map<AwtFont, File>
): Map<String, SystemFontFamily> {
val sortedFontFamilyNames = fontFamilyNames.sortedByDescending { it.length }
val fontFamilies = mutableMapOf<String, SystemFontFamily>()
val filesByFont = fontFiles.toMutableMap()
for (familyName in sortedFontFamilyNames) {
val files = filesByFont.filterKeys { font -> familyName.equals(font.getFamily(Locale.ENGLISH), ignoreCase = true) }
for ((font, _) in files) {
filesByFont.remove(font)
}
if (files.isEmpty()) {
continue
}
val fileFonts = files.map { (font, file) ->
val fontName = font.getFontName(Locale.ENGLISH)
val fontStyle = if (font.isItalic || looksItalic(fontName)) FontStyle.Italic else FontStyle.Normal
val rawWeight = fontWeightFromTextAttributeValue(font.attributes[TextAttribute.WEIGHT] as Float?)
val fontWeight = rawWeight ?: inferWeightFromName(
fontName.substringAfter(font.getFamily(Locale.ENGLISH)).split(' ', '-')
.map { it.trim().lowercase() }
.filter { it.isNotBlank() }
)
Font(file = file, weight = fontWeight, style = fontStyle) as FileFont
}
fontFamilies[familyName] = SystemFontFamily(familyName, FontFamily(fileFonts), fileFonts)
}
return fontFamilies
}
private fun looksItalic(name: String): Boolean = name.trimEnd().endsWith("italic", ignoreCase = true)
// The mappings are somewhat arbitrary, and may look wrong, but this just going in order on both sides
fun fontWeightFromTextAttributeValue(weightValue: Float?): FontWeight? =
when (weightValue) {
TextAttribute.WEIGHT_EXTRA_LIGHT -> FontWeight.Thin
TextAttribute.WEIGHT_LIGHT -> FontWeight.ExtraLight
TextAttribute.WEIGHT_DEMILIGHT -> FontWeight.Light
TextAttribute.WEIGHT_REGULAR -> FontWeight.Normal
TextAttribute.WEIGHT_SEMIBOLD -> FontWeight.Medium
TextAttribute.WEIGHT_MEDIUM -> FontWeight.SemiBold
TextAttribute.WEIGHT_BOLD -> FontWeight.Bold
TextAttribute.WEIGHT_HEAVY, TextAttribute.WEIGHT_EXTRABOLD -> FontWeight.ExtraBold
TextAttribute.WEIGHT_ULTRABOLD -> FontWeight.Black
else -> null
}
private fun inferWeightFromName(nameTokens: List<String>): FontWeight =
when {
nameTokens.any { it.startsWith("thin") || it == "100" } -> FontWeight.Thin
nameTokens.any {
it.startsWith("extralight") || it.startsWith("semilight") || it.startsWith("extra light")
|| it.startsWith("semi light") || it.startsWith("extra-light") || it.startsWith("semi-light") || it == "200"
} -> FontWeight.ExtraLight
nameTokens.any { it.startsWith("light") || it == "300" } -> FontWeight.Light
nameTokens.any { it.startsWith("medium") || it == "500" } -> FontWeight.Medium
nameTokens.any { it.startsWith("semibold") || it.startsWith("semi bold") || it.startsWith("semi-bold") || it == "600" } -> FontWeight.SemiBold
nameTokens.any { it.startsWith("bold") || it == "700" } -> FontWeight.Bold
nameTokens.any {
it.startsWith("extrabold") || it.startsWith("extra bold") || it.startsWith("extra-bold")
|| it.startsWith("heavy") || it == "800"
} -> FontWeight.ExtraBold
nameTokens.any { it.startsWith("black") || it == "900" } -> FontWeight.Black
else -> FontWeight.Normal
}
}

View File

@@ -0,0 +1,68 @@
package org.jetbrains.jewel.font
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable
internal data class MacOsSystemProfilerFontListingOutput(
@SerialName("SPFontsDataType") val fontData: List<FontData>
) {
@Serializable
internal data class FontData(
@Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("enabled") val enabled: Boolean,
@SerialName("_name") val fontFileName: String,
@SerialName("path") val path: String,
@SerialName("type") val type: FontType,
@SerialName("typefaces") val typefaces: List<Typeface>,
@Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("valid") val valid: Boolean
) {
@Serializable
internal data class Typeface(
// @Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("copy_protected") val copyProtected: Boolean,
// @SerialName("copyright") val copyright: String? = null,
// @SerialName("description") val description: String? = null,
// @SerialName("designer") val designer: String? = null,
// @Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("duplicate") val duplicate: Boolean,
// @Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("embeddable") val embeddable: Boolean,
@Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("enabled") val enabled: Boolean,
@SerialName("family") val fontFamilyName: String,
@SerialName("fullname") val fullName: String,
@SerialName("_name") val name: String,
// @Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("outline") val outline: Boolean,
@SerialName("style") val style: String,
// @SerialName("trademark") val trademark: String? = null,
// @SerialName("unique") val unique: String,
@Serializable(with = AppleYesNoBooleanSerializer::class) @SerialName("valid") val valid: Boolean,
// @SerialName("vendor") val vendor: String? = null,
// @SerialName("version") val version: String? = null
)
}
@Serializable
enum class FontType {
@SerialName("postscript") POSTSCRIPT,
@SerialName("truetype") TRUETYPE,
@SerialName("opentype") OPENTYPE,
@SerialName("bitmap") BITMAP
}
object AppleYesNoBooleanSerializer : KSerializer<Boolean> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("AppleYesNoBoolean", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Boolean = decoder.decodeString().lowercase() == "yes"
override fun serialize(encoder: Encoder, value: Boolean) {
encoder.encodeString(if (value) "yes" else "no")
}
}
}

View File

@@ -0,0 +1,10 @@
package org.jetbrains.jewel.font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.FileFont
data class SystemFontFamily(
val name: String,
val fontFamily: FontFamily,
val fonts: List<FileFont>
)

View File

@@ -0,0 +1,80 @@
package org.jetbrains.jewel.modifiers
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.IntSize
import org.jetbrains.jewel.components.ImageSlice
import org.jetbrains.jewel.components.ImageSliceValues
fun Modifier.background(image: ImageBitmap, maintainAspect: Boolean = true): Modifier {
return then(
DrawImageBackgroundModifier(image, maintainAspect, debugInspectorInfo {
name = "background"
properties["image"] = image
})
)
}
fun Modifier.background(image: ImageBitmap, slices: ImageSliceValues): Modifier =
background(ImageSlice(image, slices))
fun Modifier.background(imageSlice: ImageSlice): Modifier {
return then(
DrawImageSliceBackgroundModifier(imageSlice, debugInspectorInfo {
name = "background"
properties["image"] = imageSlice.image
properties["slices"] = imageSlice.slices
})
)
}
abstract class CustomBackgroundModifier(
inspectorInfo: InspectorInfo.() -> Unit
) : DrawModifier,
InspectorValueInfo(inspectorInfo) {
override fun ContentDrawScope.draw() {
drawBackground()
drawContent()
}
abstract fun DrawScope.drawBackground()
}
private class DrawImageBackgroundModifier(
val image: ImageBitmap,
val maintainAspect: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : CustomBackgroundModifier(inspectorInfo) {
override fun DrawScope.drawBackground() {
val width = size.width.toInt()
val height = size.height.toInt()
if (maintainAspect) {
val imageWidth = image.width
val imageHeight = image.height
val imageAspect = imageWidth.toDouble() / imageHeight
val areaAspect = width.toDouble() / height
val srcWidth = if (imageAspect > areaAspect) (imageHeight * areaAspect).toInt() else imageWidth
val srcHeight = if (imageAspect < areaAspect) (imageWidth / areaAspect).toInt() else imageHeight
drawImage(image, srcSize = IntSize(srcWidth, srcHeight), dstSize = IntSize(width, height))
} else {
drawImage(image, dstSize = IntSize(width, height))
}
}
}
private class DrawImageSliceBackgroundModifier(
val imageSlice: ImageSlice,
inspectorInfo: InspectorInfo.() -> Unit
) : CustomBackgroundModifier(inspectorInfo) {
override fun DrawScope.drawBackground() = imageSlice.draw(this)
}

View File

@@ -0,0 +1,51 @@
package org.jetbrains.jewel.styles
class ControlStyle<TAppearance, TState>(configure: ControlStyleBuilder<TAppearance, TState>.() -> Unit) {
private val variations = ControlStyleBuilder<TAppearance, TState>().build(configure)
fun appearance(state: TState, variation: Any? = null): TAppearance {
val tag = variation ?: defaultVariationTag
val states = variations[tag] ?: error("Variation '$variation' was not configured")
return states[state] ?: error("State '$state' was not configured")
}
companion object {
val defaultVariationTag = object {}
}
class ControlStyleBuilder<TAppearance, TState> {
private val variations = mutableMapOf<Any, Map<TState, TAppearance>>()
fun default(configure: ControlVariationBuilder<TAppearance, TState>.() -> Unit) {
variation(defaultVariationTag, configure)
}
fun variation(tag: Any, configure: ControlVariationBuilder<TAppearance, TState>.() -> Unit) {
require(!variations.containsKey(tag)) { "Variation '$tag' has already been registered" }
variations[tag] = ControlVariationBuilder<TAppearance, TState>(tag).build(configure)
}
fun build(configure: ControlStyleBuilder<TAppearance, TState>.() -> Unit): Map<Any, Map<TState, TAppearance>> {
configure()
return variations
}
}
class ControlVariationBuilder<TAppearance, TState>(val variation: Any?) {
private val states = mutableMapOf<TState, TAppearance>()
fun state(state: TState, appearance: TAppearance) {
require(!states.containsKey(state)) { "State '$state' has already been registered for variation '$variation'" }
states[state] = appearance
}
fun build(configure: ControlVariationBuilder<TAppearance, TState>.() -> Unit): Map<TState, TAppearance> {
configure()
return states
}
}
}

View File

@@ -0,0 +1,12 @@
package org.jetbrains.jewel.styles
import androidx.compose.runtime.compositionLocalOf
import kotlin.reflect.javaType
import kotlin.reflect.typeOf
val LocalContentAlpha = compositionLocalOf { 1f }
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> localNotProvided(): T = error("CompositionLocal value for ${typeOf<T>().javaType} was not provided")
object Styles

View File

@@ -0,0 +1,18 @@
package org.jetbrains.jewel.styles
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.text.TextStyle
val LocalTextStyle = compositionLocalOf<TextStyle> { localNotProvided() }
val Styles.text: TextStyle
@Composable
@ReadOnlyComposable
get() = LocalTextStyle.current
@Composable
fun Styles.withTextStyle(textStyle: TextStyle, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalTextStyle provides textStyle, content = content)
}

View File

@@ -0,0 +1,38 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeDesktop)
}
kotlin {
target {
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
}
}
}
dependencies {
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
implementation(projects.library)
implementation(projects.themes.toolbox)
implementation(projects.themes.intellij.standalone)
}
compose.desktop {
application {
mainClass = "org.jetbrains.jewel.sample.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg)
packageName = "Jewel Sample"
packageVersion = "1.0"
description = "Jewel Sample Application"
vendor = "JetBrains"
}
}
}

View File

@@ -0,0 +1,64 @@
package org.jetbrains.jewel.sample.controls
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import org.jetbrains.jewel.Orientation
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.toolbox.components.Divider
import org.jetbrains.jewel.theme.toolbox.components.Tab
import org.jetbrains.jewel.theme.toolbox.components.TabColumn
import org.jetbrains.jewel.theme.toolbox.components.TabScope
import org.jetbrains.jewel.theme.toolbox.components.Text
import org.jetbrains.jewel.theme.toolbox.components.rememberTabContainerState
import org.jetbrains.jewel.theme.toolbox.metrics
import org.jetbrains.jewel.theme.toolbox.styles.frame
import org.jetbrains.jewel.theme.toolbox.typography
@Composable
fun ControlsApplication() {
val backgroundColor = Styles.frame.appearance(Unit).backgroundColor
Row(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
val page = rememberTabContainerState("input")
Column {
Text(
"Categories",
style = Styles.typography.body.copy(fontWeight = FontWeight.Bold),
modifier = Modifier.padding(Styles.metrics.largePadding)
)
TabColumn(
page,
modifier = Modifier.fillMaxHeight().padding(Styles.metrics.smallPadding),
verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding)
) {
Section("input", "Input")
Section("information", "Information")
Section("navigation", "Navigation")
Section("typography", "Typography")
}
}
Divider(orientation = Orientation.Vertical)
Column(modifier = Modifier.fillMaxSize()) {
when (page.selectedKey) {
"input" -> InputControls()
"information" -> InformationControls()
"navigation" -> NavigationControls()
"typography" -> Typography()
}
}
}
}
@Composable
private fun TabScope<String>.Section(key: String, caption: String) {
Tab(key) {
Text(caption)
}
}

View File

@@ -0,0 +1,49 @@
package org.jetbrains.jewel.sample.controls
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.toolbox.components.LinearProgressIndicator
import org.jetbrains.jewel.theme.toolbox.components.Text
import org.jetbrains.jewel.theme.toolbox.components.TextField
import org.jetbrains.jewel.theme.toolbox.metrics
import org.jetbrains.jewel.theme.toolbox.typography
@Composable
fun InformationControls() {
Column(
verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding),
modifier = Modifier.fillMaxSize().padding(Styles.metrics.largePadding),
) {
Column {
val progressTarget = remember { mutableStateOf(0f) }
val progress = animateFloatAsState(progressTarget.value, animationSpec = tween(2000))
val animateProgressModifier = Modifier.clickable {
progressTarget.value = 1f - progressTarget.value
}
LinearProgressIndicator(progress.value, modifier = animateProgressModifier.width(200.dp))
Spacer(Modifier.height(Styles.metrics.smallPadding))
Text("Click for animation", style = Styles.typography.caption, modifier = animateProgressModifier)
TextField(progressTarget.value.toString(), {
progressTarget.value = it.toFloatOrNull() ?: progressTarget.value
})
Spacer(Modifier.height(Styles.metrics.largePadding))
LinearProgressIndicator(modifier = animateProgressModifier.size(400.dp, 8.dp))
}
}
}

View File

@@ -0,0 +1,102 @@
package org.jetbrains.jewel.sample.controls
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.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectableGroup
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 org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.intellij.components.Button
import org.jetbrains.jewel.theme.toolbox.components.Checkbox
import org.jetbrains.jewel.theme.toolbox.components.CheckboxRow
import org.jetbrains.jewel.theme.toolbox.components.RadioButtonRow
import org.jetbrains.jewel.theme.toolbox.components.Switch
import org.jetbrains.jewel.theme.toolbox.components.Text
import org.jetbrains.jewel.theme.toolbox.components.TextField
import org.jetbrains.jewel.theme.toolbox.metrics
enum class RadioSample {
Enabled, Disabled, Automatic, Unavailable
}
@Composable
fun InputControls() {
Column(
verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding),
modifier = Modifier.fillMaxSize().padding(Styles.metrics.largePadding),
) {
val switchState = remember { mutableStateOf(false) }
Row(
horizontalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding),
verticalAlignment = Alignment.CenterVertically
) {
Text("Work in background")
Switch(checked = switchState.value, onCheckedChange = { switchState.value = it })
}
val checkboxState1 = remember { mutableStateOf(false) }
Checkbox(checked = checkboxState1.value, onCheckedChange = { checkboxState1.value = it })
Spacer(Modifier.height(Styles.metrics.smallPadding))
val checkboxState2 = remember { mutableStateOf(false) }
CheckboxRow(checked = checkboxState2.value, onCheckedChange = { checkboxState2.value = it }) {
Text("Enable various magic", Modifier.alignByBaseline())
}
val checkboxState3 = remember { mutableStateOf(false) }
Checkbox(
"Enable dangerous features",
checked = checkboxState3.value,
onCheckedChange = { checkboxState3.value = it })
Checkbox(
"This is a checkbox\nwith multiple lines\nof content to see the alignment",
remember { mutableStateOf(false) }
)
Checkbox("Disabled", false, {}, enabled = false)
Checkbox("Checked and disabled", true, {}, enabled = false)
Spacer(Modifier.height(Styles.metrics.smallPadding))
val radioState = remember { mutableStateOf(RadioSample.Automatic) }
Column(Modifier.selectableGroup(), verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding)) {
RadioButtonRow(radioState, RadioSample.Automatic) {
Text("Automatic detection of the property", Modifier.alignByBaseline())
}
RadioButtonRow(radioState, RadioSample.Enabled) {
Text("Enable the property", Modifier.alignByBaseline())
}
RadioButtonRow(radioState, RadioSample.Disabled) {
Text("Disable the property", Modifier.alignByBaseline())
}
RadioButtonRow(radioState, RadioSample.Unavailable, enabled = false) {
Text("Unavailable", Modifier.alignByBaseline())
}
}
Spacer(Modifier.height(Styles.metrics.smallPadding))
val textFieldState = remember { mutableStateOf("Enter something…") }
TextField(textFieldState.value, { textFieldState.value = it })
Spacer(Modifier.height(Styles.metrics.largePadding))
Row(horizontalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding)) {
Button({}) {
Text("OK")
}
Button({}) {
Text("Do jump and float")
}
Button({}, enabled = false) {
Text("Cancel")
}
}
}
}

View File

@@ -0,0 +1,29 @@
package org.jetbrains.jewel.sample.controls
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.toolbox.components.Tab
import org.jetbrains.jewel.theme.toolbox.components.TabRow
import org.jetbrains.jewel.theme.toolbox.components.Text
import org.jetbrains.jewel.theme.toolbox.components.rememberTabContainerState
import org.jetbrains.jewel.theme.toolbox.metrics
@Composable
fun NavigationControls() {
Column(
verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding),
modifier = Modifier.fillMaxSize().padding(Styles.metrics.largePadding),
) {
val tabState = rememberTabContainerState(1)
TabRow(tabState) {
Tab(1) { Text("One") }
Tab(2) { Text("Two") }
Tab(3) { Text("Three") }
}
}
}

View File

@@ -0,0 +1,40 @@
package org.jetbrains.jewel.sample.controls
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.toolbox.components.Text
import org.jetbrains.jewel.theme.toolbox.metrics
import org.jetbrains.jewel.theme.toolbox.typography
@Composable
fun Typography() {
Column(
verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding),
modifier = Modifier.fillMaxSize().padding(Styles.metrics.largePadding),
) {
Text("Title of the document", style = Styles.typography.title)
Text("Subtitle with some more information", style = Styles.typography.subtitle)
Text(
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus scelerisque iaculis magna, eget convallis ante elementum nec. Nunc lobortis mauris tempor ante sollicitudin, nec ornare magna posuere. Nulla venenatis velit id dictum rutrum. Sed malesuada feugiat enim, nec ornare eros congue vitae. Sed nec feugiat lacus, non luctus magna. Aliquam nec sapien vulputate, malesuada purus eu, egestas quam. Nam mauris tellus, sagittis quis cursus et, dapibus eu odio. Mauris est ex, maximus nec dictum et, sagittis sit amet urna. In in consequat dui, faucibus egestas sem. Aliquam ut fermentum risus, vitae venenatis lacus. Nunc sit amet leo non ligula placerat iaculis dapibus ac dui.
Morbi vitae ipsum et magna tempus pharetra nec eget risus. Phasellus viverra semper ex, eu tristique massa gravida at. Etiam feugiat mi id nunc efficitur gravida. Maecenas id semper sem. Cras congue commodo elit, a viverra turpis tristique in. In feugiat eleifend imperdiet. Pellentesque vitae hendrerit ex. Nunc gravida mi non imperdiet aliquet.
""".trimIndent(), style = Styles.typography.body
)
Text(
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus scelerisque iaculis magna, eget convallis ante elementum nec. Nunc lobortis mauris tempor ante sollicitudin, nec ornare magna posuere. Nulla venenatis velit id dictum rutrum. Sed malesuada feugiat enim, nec ornare eros congue vitae. Sed nec feugiat lacus, non luctus magna. Aliquam nec sapien vulputate, malesuada purus eu, egestas quam. Nam mauris tellus, sagittis quis cursus et, dapibus eu odio. Mauris est ex, maximus nec dictum et, sagittis sit amet urna. In in consequat dui, faucibus egestas sem. Aliquam ut fermentum risus, vitae venenatis lacus. Nunc sit amet leo non ligula placerat iaculis dapibus ac dui.
Morbi vitae ipsum et magna tempus pharetra nec eget risus. Phasellus viverra semper ex, eu tristique massa gravida at. Etiam feugiat mi id nunc efficitur gravida. Maecenas id semper sem. Cras congue commodo elit, a viverra turpis tristique in. In feugiat eleifend imperdiet. Pellentesque vitae hendrerit ex. Nunc gravida mi non imperdiet aliquet.
""".trimIndent(), style = Styles.typography.smallBody
)
Text("That's all folks!", style = Styles.typography.caption)
}
}

View File

@@ -0,0 +1,135 @@
package org.jetbrains.jewel.sample
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowSize
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import org.jetbrains.jewel.sample.controls.ControlsApplication
import org.jetbrains.jewel.sample.organization.OrganizationApplication
import org.jetbrains.jewel.theme.intellij.IntelliJThemeDark
import org.jetbrains.jewel.theme.intellij.IntelliJThemeLight
import org.jetbrains.jewel.theme.toolbox.ToolboxMetrics
import org.jetbrains.jewel.theme.toolbox.ToolboxTheme
import org.jetbrains.jewel.theme.toolbox.Typography
import org.jetbrains.jewel.theme.toolbox.toolboxDarkPalette
import org.jetbrains.jewel.theme.toolbox.toolboxLightPalette
enum class Application {
Organization,
Controls
}
enum class Palette {
Light, Dark
}
enum class Theme {
Toolbox, IntelliJ
}
fun main() = application {
var theme by mutableStateOf(Theme.IntelliJ)
var palette by mutableStateOf(Palette.Light)
var metrics by mutableStateOf(ToolboxMetrics())
var selectedApplication by mutableStateOf(Application.Controls)
Window(
onCloseRequest = ::exitApplication,
title = "Jewel Sample",
state = rememberWindowState(
size = WindowSize(950.dp, 650.dp),
position = WindowPosition.Aligned(Alignment.Center)
),
) {
MenuBar {
Menu("Application") {
RadioButtonItem(
"Organization",
selected = selectedApplication == Application.Organization,
onClick = { selectedApplication = Application.Organization },
)
RadioButtonItem(
"Controls",
selected = selectedApplication == Application.Controls,
onClick = { selectedApplication = Application.Controls },
)
}
Menu("Theme") {
RadioButtonItem(
"Toolbox",
selected = theme == Theme.Toolbox,
onClick = { theme = Theme.Toolbox },
)
RadioButtonItem(
"IntelliJ",
selected = theme == Theme.IntelliJ,
onClick = { theme = Theme.IntelliJ },
)
Separator()
RadioButtonItem(
"Light",
selected = palette == Palette.Light,
onClick = { palette = Palette.Light },
)
RadioButtonItem(
"Dark",
selected = palette == Palette.Dark,
onClick = { palette = Palette.Dark },
)
Separator()
RadioButtonItem(
"Normal",
selected = metrics.base == 8.dp,
onClick = { metrics = ToolboxMetrics(8.dp) },
)
RadioButtonItem(
"Small",
selected = metrics.base == 6.dp,
onClick = { metrics = ToolboxMetrics(6.dp) },
)
RadioButtonItem(
"Large",
selected = metrics.base == 12.dp,
onClick = { metrics = ToolboxMetrics(12.dp) },
)
}
}
val toolboxPalette = when (palette) {
Palette.Light -> toolboxLightPalette
Palette.Dark -> toolboxDarkPalette
}
val toolboxTypography = Typography(metrics)
when (theme) {
Theme.Toolbox -> ToolboxTheme(toolboxPalette, metrics, toolboxTypography) {
when (selectedApplication) {
Application.Organization -> OrganizationApplication()
Application.Controls -> ControlsApplication()
}
}
Theme.IntelliJ -> when (palette) {
Palette.Light -> IntelliJThemeLight {
when (selectedApplication) {
Application.Organization -> OrganizationApplication()
Application.Controls -> ControlsApplication()
}
}
Palette.Dark -> IntelliJThemeDark {
when (selectedApplication) {
Application.Organization -> OrganizationApplication()
Application.Controls -> ControlsApplication()
}
}
}
}
}
}

View File

@@ -0,0 +1,109 @@
package org.jetbrains.jewel.sample.organization
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.Orientation
import org.jetbrains.jewel.styles.LocalTextStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.toolbox.components.Divider
import org.jetbrains.jewel.theme.toolbox.components.Tab
import org.jetbrains.jewel.theme.toolbox.components.TabColumn
import org.jetbrains.jewel.theme.toolbox.components.Text
import org.jetbrains.jewel.theme.toolbox.components.rememberTabContainerState
import org.jetbrains.jewel.theme.toolbox.metrics
import org.jetbrains.jewel.theme.toolbox.styles.frame
import org.jetbrains.jewel.theme.toolbox.typography
@Composable
fun OrganizationApplication() {
val backgroundColor = Styles.frame.appearance(Unit).backgroundColor
Row(modifier = Modifier.background(backgroundColor)) {
val page = rememberTabContainerState("Dashboard")
Column {
val columnWidth = Styles.metrics.base * 30
Column(Modifier.width(columnWidth)) {
Box(Modifier.size(columnWidth, 128.dp).padding(Styles.metrics.largePadding)) {
Image(
painterResource("organization/toolbox.svg"),
"toolbox",
modifier = Modifier.size(Styles.metrics.base * 20)
)
}
Divider()
}
TabColumn(
page,
modifier = Modifier.fillMaxHeight().width(columnWidth).padding(Styles.metrics.smallPadding),
verticalArrangement = Arrangement.spacedBy(Styles.metrics.smallPadding)
) {
Tab("Dashboard") {
Section("dashboard", "Dashboard")
}
Tab("Projects") {
Section("projects", "Projects")
}
Tab("Teams") {
Section("teams", "Teams")
}
Spacer(Modifier.weight(1f))
Tab("Notifications") {
Section("notifications", "Notifications")
}
Tab("Account") {
Section("avatar", "Ivan Ivanov")
}
}
}
Divider(orientation = Orientation.Vertical)
Column(modifier = Modifier.fillMaxSize()) {
when (page.selectedKey) {
"Dashboard" -> {
TitlePanel("Dashboard")
}
"Projects" -> {
TitlePanel("Projects")
}
"Teams" -> {
TitlePanel("Teams")
}
}
}
}
}
@Composable
private fun Section(icon: String, caption: String) {
val style = LocalTextStyle.current
Image(
painterResource("organization/$icon.svg"),
icon,
modifier = Modifier.size(Styles.metrics.largePadding),
colorFilter = ColorFilter.tint(style.color)
)
Spacer(Modifier.width(Styles.metrics.smallPadding))
Text(caption, Modifier.padding(top = 3.dp))
}
@Composable
private fun TitlePanel(title: String) {
Box(Modifier.height(128.dp).padding(Styles.metrics.mediumPadding)) {
Text(title, style = Styles.typography.subtitle)
}
Divider()
}

View File

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

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 6.5C14.5 7.88071 13.3807 9 12 9C10.6193 9 9.5 7.88071 9.5 6.5C9.5 5.11929 10.6193 4 12 4C13.3807 4 14.5 5.11929 14.5 6.5Z"
fill="#19191C"/>
<path d="M5 15C5 12.7909 6.79086 11 9 11H15C17.2091 11 19 12.7909 19 15V22H5V15Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3L5 10V21H10V15C10 13.8954 10.8954 13 12 13C13.1046 13 14 13.8954 14 15V21H19V10L12 3Z"
fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.991 5.82522L4 17H20L16.009 5.82522C15.4039 4.13096 13.7991 3 12 3C10.2009 3 8.59609 4.13096 7.991 5.82522Z" fill="#19191C"/>
<path d="M12 21.9231C10.3645 21.944 9.02152 20.6355 9 19H15C14.9785 20.6355 13.6355 21.944 12 21.9231Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.9497 2L12 6.94971L16.9497 11.8994L21.8994 6.94971L16.9497 2Z" fill="#19191C"/>
<path d="M10 12H3.101L3 5H10V12Z" fill="#19191C"/>
<path d="M10 14H3V21H10V14Z" fill="#19191C"/>
<path d="M12 14H19V21H12V14Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 6C17.1046 6 18 5.10457 18 4C18 2.89543 17.1046 2 16 2C14.8954 2 14 2.89543 14 4C14 5.10457 14.8954 6 16 6Z" fill="#19191C"/>
<path d="M18 8H15C13.3431 8 12 9.34315 12 11V12H6C4.34315 12 3 13.3431 3 15V22H12V18H21V11C21 9.34315 19.6569 8 18 8Z" fill="#19191C"/>
<path d="M10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6C9.10457 6 10 6.89543 10 8Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@@ -0,0 +1,49 @@
<svg width="264" height="72" viewBox="0 0 264 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M66.1895 27.9084L68.8542 24.947C69.9534 26.1536 71.0248 26.8665 72.3706 26.8665C73.964 26.8665 74.9532 25.907 74.9532 23.6858V11.3196H79.2939V23.8777C79.2939 26.1536 78.6345 27.8538 77.4804 29.0051C76.2995 30.1843 74.5686 30.7878 72.4807 30.7878C70.0504 30.8504 67.7278 29.7874 66.1895 27.9084H66.1895Z"
fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M80.3921 23.1921L80.3921 23.2473C80.3921 27.7439 83.661 30.8425 88.084 30.8425C90.4111 30.9292 92.6423 29.9124 94.1008 28.1003L91.7105 25.989C90.7837 26.968 89.4886 27.515 88.1392 27.4973C86.3733 27.6132 84.8112 26.3635 84.5401 24.6179L94.7871 24.6179C94.815 24.2341 94.8424 23.8503 94.8424 23.5212C94.8424 19.3533 92.5896 15.5422 87.672 15.5422C83.3866 15.5422 80.3921 18.9968 80.3921 23.1921ZM87.6716 18.8874C89.4298 18.8874 90.5286 20.1487 90.7761 22.0134L84.4848 22.0134C84.8146 20.1213 85.9407 18.8874 87.6716 18.8874Z"
fill="#19191C"/>
<path d="M96.5751 26.3455V19.3811H94.8169V15.8162H96.5751V12.0598H100.751V15.8162H104.212V19.3811H100.751V25.6599C100.751 26.6199 101.163 27.0857 102.097 27.0857C102.817 27.089 103.526 26.91 104.157 26.5652V29.9104C103.16 30.4973 102.018 30.7918 100.861 30.76C98.3055 30.76 96.5751 29.7458 96.5751 26.3455Z"
fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M105.668 11.3196H114.597C116.795 11.3196 118.525 11.9226 119.624 13.0193C120.499 13.8766 120.977 15.059 120.943 16.2825V16.3372C120.99 18.0934 120.001 19.7141 118.416 20.4778C120.641 21.3279 122.014 22.6165 122.014 25.1941V25.2488C122.014 28.7586 119.157 30.5134 114.817 30.5134H105.668V11.3196ZM116.74 16.9954C116.74 15.7341 115.751 15.0212 113.965 15.0212H109.79V19.0793H113.691C115.558 19.0793 116.74 18.4758 116.74 17.0501L116.74 16.9954ZM109.79 22.5891H114.679C116.85 22.5891 117.811 23.384 117.811 24.6731V24.7278C117.811 26.1536 116.685 26.8118 114.817 26.8118H109.79V22.5891Z"
fill="#19191C"/>
<path d="M123.005 15.816H127.181V18.7774C128.033 16.7486 129.406 15.4322 131.879 15.542V19.9019H131.659C128.884 19.9019 127.181 21.5743 127.181 25.084V30.5132H123.005V15.816Z"
fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M130.863 26.236L130.863 26.2907C130.863 29.1969 133.143 30.7877 135.918 30.7877C137.606 30.86 139.238 30.1762 140.369 28.923L140.369 30.5133L144.407 30.5133L144.407 21.9855C144.407 20.0114 143.913 18.3937 142.814 17.297C141.77 16.255 140.121 15.6515 137.841 15.6515C135.871 15.607 133.917 16.0103 132.127 16.8307L133.171 20.0114C134.462 19.4877 135.843 19.2178 137.237 19.2165C139.325 19.2165 140.396 20.1759 140.396 21.9035L140.396 22.1501C139.241 21.7402 138.023 21.5361 136.797 21.547C133.308 21.547 130.863 23.0275 130.863 26.236ZM140.451 24.5905L140.451 25.3308C140.451 26.8391 139.133 27.9084 137.182 27.9084C135.836 27.9084 134.902 27.2502 134.902 26.1262L134.902 26.0715C134.902 24.7551 136.001 24.0422 137.787 24.0422C138.704 24.0345 139.612 24.2214 140.451 24.5905Z"
fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M150.424 10.4968H146.029V14.1985H150.424V10.4968ZM150.314 15.8162H146.138V30.5134H150.314V15.8162Z" fill="#19191C"/>
<path d="M152.183 15.8161H156.358V17.9001C157.32 16.6663 158.556 15.5422 160.671 15.5422C163.831 15.5422 165.671 17.6262 165.671 20.9988V30.5134H161.496V22.3147C161.496 20.3406 160.561 19.3259 158.968 19.3259C157.375 19.3259 156.358 20.3406 156.358 22.3147V30.5134H152.183L152.183 15.8161Z"
fill="#19191C"/>
<path d="M166.084 28.5667L167.87 25.8245C169.203 26.8672 170.823 27.4794 172.513 27.5794C173.722 27.5794 174.271 27.1409 174.271 26.4827V26.428C174.271 25.5232 172.842 25.2215 171.222 24.7278C169.162 24.1248 166.826 23.1648 166.826 20.3133V20.2585C166.826 17.2698 169.244 15.5969 172.211 15.5969C174.168 15.6253 176.075 16.2155 177.705 17.2971L176.112 20.176C174.656 19.3259 173.2 18.8049 172.128 18.8049C171.112 18.8049 170.59 19.2439 170.59 19.8196V19.8743C170.59 20.697 171.991 21.0808 173.585 21.6292C175.645 22.3147 178.035 23.302 178.035 25.989V26.0442C178.035 29.3069 175.59 30.7879 172.403 30.7879C170.108 30.7698 167.884 29.9882 166.084 28.5667H166.084Z"
fill="#19191C"/>
<path d="M76.1498 44.7444H67.2275V38.8074H91.5232V44.7444H82.601V68.074H76.1498V44.7444Z" fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M90.7681 53.4407V53.5242C90.7681 61.8445 97.2609 68.5759 106.309 68.5759C115.357 68.5759 121.933 61.761 121.933 53.4407V53.3572C121.933 45.0374 115.44 38.3059 106.392 38.3059C97.3445 38.3059 90.7681 45.1208 90.7681 53.4407ZM115.189 53.4407V53.5242H115.189C115.189 58.5418 111.587 62.6389 106.392 62.6389C101.199 62.6389 97.5123 58.4578 97.5123 53.4407V53.3572C97.5123 48.3401 101.114 44.2429 106.309 44.2429C111.503 44.2429 115.189 48.4236 115.189 53.4407Z"
fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M123.231 53.4407V53.5242C123.231 61.8445 129.724 68.5759 138.772 68.5759C147.82 68.5759 154.397 61.761 154.397 53.4407V53.3572C154.397 45.0374 147.904 38.3059 138.856 38.3059C129.808 38.3059 123.231 45.1208 123.231 53.4407ZM147.653 53.4407V53.5242C147.653 58.5418 144.05 62.6389 138.856 62.6389C133.662 62.6389 129.976 58.4578 129.976 53.4407V53.3572C129.976 48.3401 133.578 44.2429 138.772 44.2429C143.966 44.2429 147.653 48.4236 147.653 53.4407Z"
fill="#19191C"/>
<path d="M156.993 38.8074H163.443V62.2205H178.063V68.074H156.993V38.8074Z" fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M179.779 38.8074H193.393C196.744 38.8074 199.383 39.7273 201.059 41.3997C202.393 42.7072 203.121 44.5099 203.069 46.3752V46.4587C203.069 49.7615 201.31 51.6013 199.216 52.7717C202.608 54.0681 204.703 56.033 204.703 59.9632V60.0466C204.703 65.3982 200.346 68.074 193.728 68.074H179.779V38.8074ZM196.661 47.4622C196.661 45.5388 195.153 44.4519 192.43 44.4519H186.063V50.6394H192.011C194.859 50.6394 196.66 49.72 196.66 47.5457L196.661 47.4622ZM186.062 55.991H193.518H193.518C196.827 55.991 198.294 57.2039 198.294 59.1687V59.2522C198.294 61.4265 196.576 62.4299 193.728 62.4299H186.062V55.991Z"
fill="#19191C"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M205.58 53.4407V53.5242C205.58 61.8445 212.073 68.5759 221.121 68.5759C230.169 68.5759 236.745 61.761 236.745 53.4407V53.3572C236.745 45.0374 230.252 38.3059 221.205 38.3059C212.157 38.3059 205.58 45.1208 205.58 53.4407ZM230.001 53.4407V53.5242H230.001C230.001 58.5418 226.399 62.6389 221.204 62.6389C216.011 62.6389 212.324 58.4578 212.324 53.4407V53.3572C212.324 48.3401 215.926 44.2429 221.121 44.2429C226.315 44.2429 230.001 48.4236 230.001 53.4407Z"
fill="#19191C"/>
<path d="M262.675 38.8074L252.873 53.1062L263.093 68.074H255.554L248.977 58.0398L242.359 68.074H235.028L245.249 53.1902L235.447 38.8074H242.987L249.102 48.2981L255.344 38.8074H262.675Z"
fill="#19191C"/>
<path d="M26.5527 64.2181L53.1047 48.8931V18.2676L26.5527 33.5926V64.2181Z" fill="#19191C"/>
<path d="M43.2922 48.9396L31.7212 55.5791V51.3456L43.2922 44.7056V48.9396Z" fill="white"/>
<path d="M26.552 3L0 18.2675L0.000500952 18.268L0 18.2675V48.893L26.552 64.218V33.5925L53.1045 18.2675L26.552 3Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="12.1904" y1="77.7624" x2="67.4608" y2="56.1979" gradientUnits="userSpaceOnUse">
<stop offset="0.04301" stop-color="#FF8618"/>
<stop offset="0.38172" stop-color="#FF246E"/>
<stop offset="0.98925" stop-color="#AF1DF5"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,19 @@
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = "jewel"
include(
":library",
":sample",
":themes:toolbox",
":themes:intellij",
":themes:intellij:standalone",
":themes:intellij:idea"
)

View File

@@ -0,0 +1,22 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeDesktop)
}
kotlin {
target {
compilations.all {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
}
}
dependencies {
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
api(projects.library)
}

View File

@@ -0,0 +1,40 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeDesktop)
alias(libs.plugins.ideaGradlePlugin)
}
kotlin {
target {
compilations.all {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
}
}
}
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
}
}
}
intellij {
// pluginName.set("Compose support for IJ UI development")
version.set("LATEST-EAP-SNAPSHOT")
plugins.set(listOf("org.jetbrains.kotlin", "org.jetbrains.compose.desktop.ide:1.0.0"))
version.set("2021.3.1")
}
dependencies {
compileOnly(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
implementation(projects.themes.intellij) {
exclude(compose.desktop.currentOs)
}
implementation(projects.library) {
exclude(compose.desktop.currentOs)
}
}

View File

@@ -0,0 +1,123 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
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.platform.Typeface
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.project.Project
import com.intellij.util.ui.DirProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import org.jetbrains.skiko.toSkikoTypeface
import javax.swing.UIManager
import java.awt.Color as AwtColor
@Suppress("UnstableApiUsage")
@ExperimentalCoroutinesApi
val Project.lafChangesFlow
get() = callbackFlow {
val connection = messageBus.simpleConnect()
connection.subscribe(
LafManagerListener.TOPIC,
LafManagerListener { trySend(Unit) }
)
awaitClose { connection.disconnect() }
}
@Composable
fun IntelliJTheme(project: Project, content: @Composable () -> Unit) {
val themeDefinitionFlow by derivedStateOf {
project.lafChangesFlow.map { CurrentIntelliJThemeDefinition() }
}
val themeDefinition by themeDefinitionFlow.collectAsState(CurrentIntelliJThemeDefinition())
IntelliJTheme(
palette = themeDefinition.palette,
metrics = themeDefinition.metrics,
painters = themeDefinition.painters,
typography = themeDefinition.typography,
content = content
)
}
internal fun AwtColor.toColor() = Color(red, green, blue, alpha)
internal fun retrieveFloat(key: String) =
UIManager.get(key) as? Float ?: error("Float with key '$key' not found")
internal fun retrieveColor(key: String) =
retrieveColorOrNull(key) ?: error("Color with key '$key' not found")
internal fun retrieveColorOrNull(key: String) =
UIManager.getColor(key)?.toColor()
private val dirProvider = DirProvider()
internal fun lookupSvgIcon(
name: String,
selected: Boolean = false,
focused: Boolean = false,
enabled: Boolean = true,
editable: Boolean = false,
pressed: Boolean = false
): @Composable () -> Painter {
var key = name
if (editable) {
key += "Editable"
}
if (selected) {
key += "Selected"
}
when {
pressed -> key += "Pressed"
focused -> key += "Focused"
!enabled -> key += "Disabled"
}
// for Mac blue theme and other LAFs use default directory icons
val dir = dirProvider.dir()
val path = "$dir$key.svg"
return {
rememberSvgResource(path.removePrefix("/"), dirProvider.javaClass.classLoader)
}
}
internal fun retrieveColors(vararg keys: String) = keys.map { retrieveColor(it) }
internal fun retrieveIntAsDp(key: String) = UIManager.getInt(key).dp
internal fun retrieveInsetsAsPaddingValues(key: String) =
UIManager.getInsets(key)
.let { PaddingValues(it.left.dp, it.top.dp, it.right.dp, it.bottom.dp) }
suspend fun retrieveFont(
key: String,
color: Color = Color.Unspecified,
lineHeight: TextUnit = TextUnit.Unspecified
) = with(UIManager.getFont(key)) {
TextStyle(
color = color,
fontSize = size.sp,
fontWeight = FontWeight.Normal,
fontFamily = FontFamily(Typeface(toSkikoTypeface()!!)),
// todo textDecoration might be defined in the awt theme
lineHeight = lineHeight
)
}

View File

@@ -0,0 +1,152 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.runBlocking
@Suppress("FunctionName")
fun CurrentIntelliJThemeDefinition(): IntelliJThemeDefinition {
val buttonPalette = IntelliJPalette.Button(
background = Brush.verticalGradient(retrieveColors("Button.startBackground", "Button.endBackground")),
foreground = retrieveColor("Button.foreground"),
foregroundDisabled = retrieveColor("Button.disabledText"),
shadow = retrieveColorOrNull("Button.default.shadowColor") ?: Color.Unspecified,
stroke = Brush.verticalGradient(retrieveColors("Button.startBorderColor", "Button.endBorderColor")),
strokeFocused = retrieveColor("Button.focusedBorderColor"),
strokeDisabled = retrieveColor("Button.disabledBorderColor"),
defaultBackground = Brush.verticalGradient(
retrieveColors(
"Button.default.startBackground",
"Button.default.endBackground"
)
),
defaultForeground = retrieveColor("Button.default.foreground"),
defaultStroke = Brush.verticalGradient(
retrieveColors(
"Button.default.startBorderColor",
"Button.default.endBorderColor"
)
),
defaultStrokeFocused = retrieveColor("Button.default.focusedBorderColor"),
defaultShadow = retrieveColorOrNull("Button.default.shadowColor") ?: Color.Unspecified
)
val textFieldPalette = IntelliJPalette.TextField(
background = retrieveColor("TextField.background"),
backgroundDisabled = retrieveColor("TextField.disabledBackground"),
foreground = retrieveColor("TextField.foreground"),
foregroundDisabled = retrieveColor("Label.disabledForeground")
)
val palette = IntelliJPalette(
button = buttonPalette,
background = retrieveColor("Panel.background"),
text = retrieveColor("Panel.foreground"),
textDisabled = retrieveColor("Label.disabledForeground"),
controlStroke = retrieveColor("Component.borderColor"),
controlStrokeDisabled = retrieveColor("Component.disabledBorderColor"),
controlStrokeFocused = retrieveColor("Component.focusedBorderColor"),
controlFocusHalo = retrieveColor("Component.focusColor"),
controlInactiveHaloError = retrieveColor("Component.inactiveErrorFocusColor"),
controlInactiveHaloWarning = retrieveColor("Component.inactiveWarningFocusColor"),
controlHaloError = retrieveColor("Component.errorFocusColor"),
controlHaloWarning = retrieveColor("Component.warningFocusColor"),
checkbox = IntelliJPalette.Checkbox(
background = retrieveColor("CheckBox.background"),
foreground = retrieveColor("CheckBox.foreground"),
foregroundDisabled = retrieveColor("CheckBox.disabledText")
),
radioButton = IntelliJPalette.RadioButton(
background = retrieveColor("RadioButton.background"),
foreground = retrieveColor("RadioButton.foreground"),
foregroundDisabled = retrieveColor("RadioButton.disabledText")
),
textField = textFieldPalette,
separator = IntelliJPalette.Separator(
color = retrieveColor("Separator.foreground"),
background = retrieveColor("Separator.background")
),
scrollbar = IntelliJPalette.Scrollbar(
thumbHoverColor = retrieveColor("ScrollBar.foreground"),
thumbIdleColor = retrieveColor("ScrollBar.thumbHighlight")
)
)
val metrics = IntelliJMetrics(
gridSize = 8.dp,
singlePadding = 8.dp,
doublePadding = 16.dp,
controlFocusHaloWidth = retrieveIntAsDp("Component.focusWidth"),
controlArc = retrieveIntAsDp("Component.arc"),
button = IntelliJMetrics.Button(
strokeWidth = 1.dp,
arc = CornerSize(retrieveIntAsDp("Button.arc")),
padding = retrieveInsetsAsPaddingValues("Button.margin"),
),
controlFocusHaloArc = retrieveIntAsDp("Component.arc"),
separator = IntelliJMetrics.Separator(
strokeWidth = 1.dp
),
scrollbar = IntelliJMetrics.Scrollbar(
minSize = 29.dp,
thickness = 7.dp,
thumbCornerSize = CornerSize(4.dp)
)
)
val painters = IntelliJPainters(
checkbox = IntelliJPainters.CheckboxPainters(
unselected = lookupSvgIcon(name = "checkBox", selected = false, focused = false, enabled = true),
unselectedDisabled = lookupSvgIcon(name = "checkBox", selected = false, focused = false, enabled = false),
unselectedFocused = lookupSvgIcon(name = "checkBox", selected = false, focused = true, enabled = true),
selected = lookupSvgIcon(name = "checkBox", selected = true, focused = false, enabled = true),
selectedDisabled = lookupSvgIcon(name = "checkBox", selected = true, focused = false, enabled = false),
selectedFocused = lookupSvgIcon(name = "checkBox", selected = true, focused = true, enabled = true),
indeterminate = lookupSvgIcon(
name = "checkBoxIndeterminate",
selected = true,
focused = false,
enabled = true
),
indeterminateDisabled = lookupSvgIcon(
name = "checkBoxIndeterminate",
selected = true,
focused = false,
enabled = false
),
indeterminateFocused = lookupSvgIcon(
name = "checkBoxIndeterminate",
selected = true,
focused = true,
enabled = true
)
),
radioButton = IntelliJPainters.RadioButtonPainters(
unselected = lookupSvgIcon(name = "radio", selected = false, focused = false, enabled = true),
unselectedDisabled = lookupSvgIcon(name = "radio", selected = false, focused = false, enabled = false),
unselectedFocused = lookupSvgIcon(name = "radio", selected = false, focused = true, enabled = true),
selected = lookupSvgIcon(name = "radio", selected = true, focused = false, enabled = true),
selectedDisabled = lookupSvgIcon(name = "radio", selected = true, focused = false, enabled = false),
selectedFocused = lookupSvgIcon(name = "radio", selected = true, focused = true, enabled = true)
)
)
val typography = runBlocking {
IntelliJTypography(
default = retrieveFont("Panel.font", palette.text),
button = retrieveFont("Button.font", palette.button.foreground),
checkBox = retrieveFont("CheckBox.font", palette.checkbox.foreground),
radioButton = retrieveFont("RadioButton.font", palette.radioButton.foreground),
textField = retrieveFont("TextField.font", palette.textField.foreground)
)
}
return IntelliJThemeDefinition(
palette = palette,
metrics = metrics,
typography = typography,
painters = painters
)
}

View File

@@ -0,0 +1,135 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.unit.dp
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import org.jetbrains.jewel.theme.intellij.components.Button
import org.jetbrains.jewel.theme.intellij.components.Checkbox
import org.jetbrains.jewel.theme.intellij.components.CheckboxRow
import org.jetbrains.jewel.theme.intellij.components.Text
internal class ProjectLifecycle : Disposable, CoroutineScope {
override val coroutineContext = SupervisorJob()
override fun dispose() = cancel()
}
@ExperimentalCoroutinesApi
internal class JewelDemoToolWindow : ToolWindowFactory, DumbAware {
enum class RadioSample {
Enabled, Disabled, Automatic, Unavailable
}
@OptIn(ExperimentalComposeUiApi::class)
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
toolWindow.addComposeTab("Compose Demo") {
IntelliJTheme(project) {
Box(
modifier = Modifier
.fillMaxSize()
.background(IntelliJTheme.palette.background),
contentAlignment = Alignment.Center
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterVertically)) {
var clicks by remember { mutableStateOf(0) }
Button({ clicks++ }) {
Text("Hello world, $clicks")
}
var checked by remember { mutableStateOf(false) }
CheckboxRow(
checked = checked,
onCheckedChange = { checked = it }
) {
Text("Hello, I am a themed checkbox")
}
val textFieldState = remember { mutableStateOf("I am a textfield") }
// TextField(textFieldState.value, { textFieldState.value = it })
val radioState = remember { mutableStateOf(RadioSample.Automatic) }
Column(Modifier.selectableGroup(), verticalArrangement = Arrangement.spacedBy(IntelliJTheme.metrics.singlePadding)) {
// RadioButtonRow(radioState, RadioSample.Automatic) {
// Text("Automatic detection of the property", Modifier.alignByBaseline())
// }
// RadioButtonRow(radioState, RadioSample.Enabled) {
// Text("Enable the property", Modifier.alignByBaseline())
// }
// RadioButtonRow(radioState, RadioSample.Disabled) {
// Text("Disable the property", Modifier.alignByBaseline())
// }
// RadioButtonRow(radioState, RadioSample.Unavailable, enabled = false) {
// Text("Unavailable", Modifier.alignByBaseline())
// }
}
}
}
}
}
toolWindow.addComposeTab("Compose Demo 2") {
IntelliJTheme(project) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
var checked by remember { mutableStateOf(true) }
Column {
Button({}) {
Text("Hello world 2")
}
Checkbox(
checked = checked,
onCheckedChange = { checked = it }
)
}
}
}
}
}
}
internal fun ToolWindow.addComposeTab(
displayName: String,
isLockable: Boolean = true,
content: @Composable () -> Unit
) = ComposePanel(content = content)
.also { contentManager.addContent(contentManager.factory.createContent(it, displayName, isLockable)) }
internal fun ComposePanel(
height: Int = 800,
width: Int = 800,
y: Int = 0,
x: Int = 0,
content: @Composable () -> Unit
): ComposePanel {
val panel = ComposePanel()
panel.setBounds(x, y, width, height)
panel.setContent(content)
return panel
}

View File

@@ -0,0 +1,36 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadSvgPainter
import java.io.InputStream
@Composable
fun rememberSvgResource(resourcePath: String, classLoader: ClassLoader): Painter {
val density = LocalDensity.current
return remember(resourcePath, density) {
useResource(resourcePath, classLoader) {
loadSvgPainter(it, density)
}
}
}
inline fun <T> useResource(
resourcePath: String,
classLoader: ClassLoader,
block: (InputStream) -> T
): T = openResource(resourcePath, classLoader).use(block)
/**
* Open [InputStream] from a resource stored in resources for the application.
*
* @throws IllegalArgumentException if there is no [resourcePath] in resources
*/
@PublishedApi
internal fun openResource(resourcePath: String, classLoader: ClassLoader): InputStream {
return requireNotNull(classLoader.getResourceAsStream(resourcePath)) {
"Resource $resourcePath not found"
}
}

View File

@@ -0,0 +1,17 @@
<idea-plugin implementation-detail="false">
<id>org.jetbrains.jewel</id>
<name>Jewel Font Preloader</name>
<vendor>JetBrains</vendor>
<dependencies>
<plugin id="org.jetbrains.compose.desktop.ide"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<projectService serviceImplementation="org.jetbrains.jewel.theme.intellij.ProjectLifecycle"/>
<toolWindow id="JewelDemo" anchor="bottom" secondary="false"
canCloseContents="true"
factoryClass="org.jetbrains.jewel.theme.intellij.JewelDemoToolWindow"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,45 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.unit.Dp
class IntelliJMetrics(
val gridSize: Dp,
val singlePadding: Dp,
val doublePadding: Dp,
val controlFocusHaloWidth: Dp, // Component.focusWidth
val controlFocusHaloArc: Dp, // Component.focusWidth
val controlArc: Dp, // Component.arc
val button: Button,
val separator: Separator,
val scrollbar: Scrollbar,
) {
data class Button(
val strokeWidth: Dp, // N/A in Swing
val arc: CornerSize, // Button.arc
val padding: PaddingValues // Button.margin
) {
companion object
}
data class Separator(
val strokeWidth: Dp, // N/A in Swing
) {
companion object
}
data class Scrollbar(
val minSize: Dp, // ScrollBar.minimumThumbSize
val thickness: Dp, // N/A in Swing
val thumbCornerSize: CornerSize, // See com.intellij.ui.components.ScrollBarPainter.Thumb.paint
) {
companion object
}
companion object
}

View File

@@ -0,0 +1,92 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
class IntelliJPainters(
val checkbox: CheckboxPainters,
val radioButton: RadioButtonPainters,
) {
companion object {
val light = IntelliJPainters(
checkbox = CheckboxPainters.light,
radioButton = RadioButtonPainters.light
)
val darcula = IntelliJPainters(
checkbox = CheckboxPainters.dark,
radioButton = RadioButtonPainters.dark
)
}
data class CheckboxPainters(
val unselected: (@Composable () -> Painter),
val unselectedDisabled: (@Composable () -> Painter),
val unselectedFocused: (@Composable () -> Painter),
val selected: (@Composable () -> Painter),
val selectedDisabled: (@Composable () -> Painter),
val selectedFocused: (@Composable () -> Painter),
val indeterminate: (@Composable () -> Painter),
val indeterminateDisabled: (@Composable () -> Painter),
val indeterminateFocused: (@Composable () -> Painter),
) {
companion object {
val light = CheckboxPainters(
unselected = { painterResource("intellij/checkBox.svg") },
unselectedDisabled = { painterResource("intellij/checkBoxDisabled.svg") },
unselectedFocused = { painterResource("intellij/checkBoxFocused.svg") },
selected = { painterResource("intellij/checkBoxSelected.svg") },
selectedDisabled = { painterResource("intellij/checkBoxSelectedDisabled.svg") },
selectedFocused = { painterResource("intellij/checkBoxSelectedFocused.svg") },
indeterminate = { painterResource("intellij/checkBoxIndeterminateSelected.svg") },
indeterminateDisabled = { painterResource("intellij/checkBoxIndeterminateSelectedDisabled.svg") },
indeterminateFocused = { painterResource("intellij/checkBoxIndeterminateSelectedFocused.svg") },
)
val dark = CheckboxPainters(
unselected = { painterResource("darcula/checkBox.svg") },
unselectedDisabled = { painterResource("darcula/checkBoxDisabled.svg") },
unselectedFocused = { painterResource("darcula/checkBoxFocused.svg") },
selected = { painterResource("darcula/checkBoxSelected.svg") },
selectedDisabled = { painterResource("darcula/checkBoxSelectedDisabled.svg") },
selectedFocused = { painterResource("darcula/checkBoxSelectedFocused.svg") },
indeterminate = { painterResource("darcula/checkBoxIndeterminateSelected.svg") },
indeterminateDisabled = { painterResource("darcula/checkBoxIndeterminateSelectedDisabled.svg") },
indeterminateFocused = { painterResource("darcula/checkBoxIndeterminateSelectedFocused.svg") },
)
}
}
data class RadioButtonPainters(
val unselected: (@Composable () -> Painter),
val unselectedDisabled: (@Composable () -> Painter),
val unselectedFocused: (@Composable () -> Painter),
val selected: (@Composable () -> Painter),
val selectedDisabled: (@Composable () -> Painter),
val selectedFocused: (@Composable () -> Painter),
) {
companion object {
val light = RadioButtonPainters(
unselected = { painterResource("intellij/radio.svg") },
unselectedDisabled = { painterResource("intellij/radioDisabled.svg") },
unselectedFocused = { painterResource("intellij/radioFocused.svg") },
selected = { painterResource("intellij/radioSelected.svg") },
selectedDisabled = { painterResource("intellij/radioSelectedDisabled.svg") },
selectedFocused = { painterResource("intellij/radioSelectedFocused.svg") },
)
val dark = RadioButtonPainters(
unselected = { painterResource("darcula/radio.svg") },
unselectedDisabled = { painterResource("darcula/radioDisabled.svg") },
unselectedFocused = { painterResource("darcula/radioFocused.svg") },
selected = { painterResource("darcula/radioSelected.svg") },
selectedDisabled = { painterResource("darcula/radioSelectedDisabled.svg") },
selectedFocused = { painterResource("darcula/radioSelectedFocused.svg") },
)
}
}
}

View File

@@ -0,0 +1,94 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
data class IntelliJPalette(
val button: Button,
val checkbox: Checkbox,
val radioButton: RadioButton,
val textField: TextField,
val separator: Separator,
val background: Color, // Panel.background
val text: Color, // Panel.foreground
val textDisabled: Color, // Label.disabledForeground
val controlStroke: Color, // Component.borderColor
val controlStrokeDisabled: Color, // Component.disabledBorderColor
val controlStrokeFocused: Color, // Component.focusedBorderColor
val controlFocusHalo: Color, // Component.focusColor
val controlInactiveHaloError: Color, // Component.inactiveErrorFocusColor
val controlInactiveHaloWarning: Color, // Component.inactiveWarningFocusColor
val controlHaloError: Color, // Component.errorFocusColor
val controlHaloWarning: Color, // Component.warningFocusColor
val scrollbar: Scrollbar,
) {
data class RadioButton(
val background: Color,
val foreground: Color,
val foregroundDisabled: Color
) {
companion object
}
data class Checkbox(
val background: Color, // Checkbox.background
val foreground: Color,
val foregroundDisabled: Color
) {
companion object
}
data class TextField(
val background: Color,
val backgroundDisabled: Color,
val foreground: Color,
val foregroundDisabled: Color
) {
companion object
}
data class Button(
val background: Brush, // Button.startBackground and Button.endBackground
val foreground: Color, // Button.foreground
val foregroundDisabled: Color, // Button.disabledText
val shadow: Color, // Button.default.shadowColor
val stroke: Brush, // Button.startBorderColor and Button.endBorderColor
val strokeFocused: Color, // Button.focusedBorderColor
val strokeDisabled: Color, // Button.disabledBorderColor
val defaultBackground: Brush, // Button.default.startBackground and Button.default.endBackground
val defaultForeground: Color, // Button.default.foreground
val defaultStroke: Brush, // Button.default.startBorderColor and Button.default.endBorderColor
val defaultStrokeFocused: Color, // Button.default.focusedBorderColor
val defaultShadow: Color, // Button.default.shadowColor
) {
companion object
}
data class Separator(
val color: Color, // Separator.separatorColor
val background: Color, // Separator.background
) {
companion object
}
data class Scrollbar(
val thumbHoverColor: Color, // See com.intellij.ui.components.ScrollBarPainter.THUMB_BACKGROUND
val thumbIdleColor: Color, // See com.intellij.ui.components.ScrollBarPainter.THUMB_HOVERED_BACKGROUND
) {
companion object
}
companion object
}

View File

@@ -0,0 +1,70 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.LocalScrollbarStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import org.jetbrains.jewel.NoIndication
import org.jetbrains.jewel.styles.LocalTextStyle
import org.jetbrains.jewel.styles.localNotProvided
import org.jetbrains.jewel.theme.intellij.styles.ButtonStyle
import org.jetbrains.jewel.theme.intellij.styles.CheckboxStyle
import org.jetbrains.jewel.theme.intellij.styles.FrameStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalButtonStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalCheckboxStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalFrameStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalIconButtonStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalSeparatorStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalTextFieldStyle
import org.jetbrains.jewel.theme.intellij.styles.ScrollbarStyle
import org.jetbrains.jewel.theme.intellij.styles.SeparatorStyle
import org.jetbrains.jewel.theme.intellij.styles.TextFieldStyle
val LocalTypography = compositionLocalOf<IntelliJTypography> { localNotProvided() }
val LocalMetrics = compositionLocalOf<IntelliJMetrics> { localNotProvided() }
val LocalPainters = compositionLocalOf<IntelliJPainters> { localNotProvided() }
val LocalPalette = compositionLocalOf<IntelliJPalette> { localNotProvided() }
@Composable
fun IntelliJTheme(
palette: IntelliJPalette,
metrics: IntelliJMetrics,
painters: IntelliJPainters,
typography: IntelliJTypography,
content: @Composable () -> Unit
) = CompositionLocalProvider(
LocalFrameStyle provides FrameStyle(palette),
LocalTextStyle provides typography.default,
LocalButtonStyle provides ButtonStyle(palette, metrics, typography.button),
LocalIconButtonStyle provides ButtonStyle(palette, metrics, typography.button),
LocalCheckboxStyle provides CheckboxStyle(palette, painters, typography.checkBox),
LocalTextFieldStyle provides TextFieldStyle(palette, metrics, typography.textField),
LocalSeparatorStyle provides SeparatorStyle(palette, metrics),
LocalScrollbarStyle provides ScrollbarStyle(palette, metrics),
LocalIndication provides NoIndication,
LocalTypography provides typography,
LocalMetrics provides metrics,
LocalPainters provides painters,
LocalPalette provides palette,
content = content
)
object IntelliJTheme {
val typography
@Composable
get() = LocalTypography.current
val metrics
@Composable
get() = LocalMetrics.current
val painters
@Composable
get() = LocalPainters.current
val palette
@Composable
get() = LocalPalette.current
}

View File

@@ -0,0 +1,8 @@
package org.jetbrains.jewel.theme.intellij
data class IntelliJThemeDefinition(
val palette: IntelliJPalette,
val metrics: IntelliJMetrics,
val typography: IntelliJTypography,
val painters: IntelliJPainters
)

View File

@@ -0,0 +1,14 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.ui.text.TextStyle
data class IntelliJTypography(
val default: TextStyle,
val button: TextStyle,
val checkBox: TextStyle,
val radioButton: TextStyle,
val textField: TextStyle
) {
companion object
}

View File

@@ -0,0 +1,186 @@
@file:OptIn(ExperimentalComposeUiApi::class)
package org.jetbrains.jewel.theme.intellij.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
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.LaunchedEffect
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.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.semantics.Role
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.jetbrains.jewel.components.ImageSliceValues
import org.jetbrains.jewel.components.state.ButtonMouseState
import org.jetbrains.jewel.components.state.ButtonState
import org.jetbrains.jewel.modifiers.background
import org.jetbrains.jewel.shape
import org.jetbrains.jewel.styles.LocalTextStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.withTextStyle
import org.jetbrains.jewel.theme.intellij.styles.ButtonAppearance
import org.jetbrains.jewel.theme.intellij.styles.ButtonStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalButtonStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalIconButtonStyle
import org.jetbrains.jewel.theme.intellij.styles.updateButtonAppearanceTransition
@Composable
fun IconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: ButtonStyle = LocalIconButtonStyle.current,
content: @Composable RowScope.() -> Unit
) = Button(onClick, modifier, enabled, focusable, interactionSource, style, content = content)
@Composable
fun ImageButton(
image: ImageBitmap,
slices: ImageSliceValues,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: ButtonStyle = LocalButtonStyle.current,
content: @Composable (RowScope.() -> Unit)
) {
val appearance = style.appearance(ButtonState())
Box(
modifier = modifier
.clickable(
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
).background(image, slices),
propagateMinConstraints = true
) {
ButtonContent(appearance, content)
}
}
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: ButtonStyle = LocalButtonStyle.current,
variation: Any? = null,
content: @Composable RowScope.() -> Unit,
) {
var isHovered by remember { mutableStateOf(false) }
var buttonState by remember(interactionSource, enabled) { mutableStateOf(ButtonState(ButtonMouseState.None, enabled)) }
LaunchedEffect(interactionSource) {
interactionSource.interactions.onEach { interaction ->
when (interaction) {
is PressInteraction.Press -> buttonState = buttonState.copy(mouse = ButtonMouseState.Pressed)
is PressInteraction.Cancel, is PressInteraction.Release -> buttonState = buttonState.copy(
mouse = if (isHovered)
ButtonMouseState.Hovered
else
ButtonMouseState.None
)
is FocusInteraction.Focus -> buttonState = buttonState.copy(focused = true)
is FocusInteraction.Unfocus -> buttonState = buttonState.copy(focused = false)
}
}.launchIn(this)
}
val appearance = style.appearance(buttonState, variation)
val appearanceTransition = updateButtonAppearanceTransition(appearance)
val shapeModifier = if (appearanceTransition.shapeStroke != null || appearanceTransition.background != null)
Modifier.shape(appearance.shape, appearanceTransition.shapeStroke, appearanceTransition.background)
else
Modifier
val haloStroke = appearanceTransition.haloStroke
val haloModifier = if (haloStroke != null)
Modifier.drawBehind {
val outline = appearance.haloShape.createOutline(size, layoutDirection, this)
drawOutline(
outline = outline,
brush = haloStroke.brush,
style = Stroke(haloStroke.width.toPx()),
)
}
else
Modifier
val pointerModifier = if (enabled)
Modifier.pointerMoveFilter(
onEnter = {
isHovered = true
buttonState = buttonState.copy(mouse = ButtonMouseState.Hovered)
false
},
onExit = {
isHovered = false
buttonState = buttonState.copy(mouse = ButtonMouseState.None)
false
})
else
Modifier
Box(
modifier
.clickable(
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
)
.focusable(
enabled = enabled && focusable,
interactionSource = interactionSource
)
.then(pointerModifier)
.then(shapeModifier)
.then(haloModifier)
.clip(appearance.shape),
propagateMinConstraints = true
) {
ButtonContent(appearance, content)
}
}
@Composable
private fun ButtonContent(appearance: ButtonAppearance, content: @Composable (RowScope.() -> Unit)) {
Styles.withTextStyle(LocalTextStyle.current.merge(appearance.textStyle)) {
Row(
Modifier
.padding(appearance.contentPadding)
.defaultMinSize(appearance.minWidth, appearance.minHeight),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}

View File

@@ -0,0 +1,299 @@
package org.jetbrains.jewel.theme.intellij.components
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.triStateToggleable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
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.drawBehind
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.painter.Painter
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.pointerMoveFilter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import kotlinx.coroutines.flow.collect
import org.jetbrains.jewel.components.state.ButtonMouseState
import org.jetbrains.jewel.components.state.CheckboxState
import org.jetbrains.jewel.shape
import org.jetbrains.jewel.styles.LocalTextStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.withTextStyle
import org.jetbrains.jewel.theme.intellij.styles.CheckboxStyle
import org.jetbrains.jewel.theme.intellij.styles.LocalCheckboxStyle
@Composable
fun Checkbox(
state: ToggleableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
variation: Any? = null,
) {
CheckboxImpl(
state, onClick, modifier, enabled, focusable, interactionSource, style, variation
) { controlModifier, designModifier, _, painter, _, _ ->
Box(controlModifier.then(designModifier)) {
if (painter != null)
Box(Modifier.paint(painter).fillMaxSize())
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CheckboxImpl(
state: ToggleableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
variation: Any? = null,
content: @Composable (Modifier, Modifier, Int, Painter?, TextStyle, Dp) -> Unit
) {
var isHovered by remember { mutableStateOf(false) }
var interactionState by remember(state, interactionSource, enabled) {
mutableStateOf(CheckboxState(state, ButtonMouseState.None, enabled = enabled))
}
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> interactionState = interactionState.copy(mouse = ButtonMouseState.Pressed)
is PressInteraction.Cancel, is PressInteraction.Release -> interactionState = interactionState.copy(
mouse = if (isHovered)
ButtonMouseState.Hovered
else
ButtonMouseState.None
)
is FocusInteraction.Focus -> interactionState = interactionState.copy(focused = true)
is FocusInteraction.Unfocus -> interactionState = interactionState.copy(focused = false)
}
}
}
val appearance = style.appearance(interactionState, variation)
val checkboxPainter = appearance.interiorPainter?.invoke()
val pointerModifier = if (enabled)
Modifier.pointerMoveFilter(
onEnter = {
isHovered = true
interactionState = interactionState.copy(mouse = ButtonMouseState.Hovered)
false
},
onExit = {
isHovered = false
interactionState = interactionState.copy(mouse = ButtonMouseState.None)
false
})
else
Modifier
val clickModifier = Modifier.triStateToggleable(
state = state,
onClick = { onClick() },
enabled = enabled,
role = Role.Checkbox,
interactionSource = interactionSource,
indication = null
)
.then(pointerModifier)
.focusable(
enabled = enabled && focusable,
interactionSource = interactionSource
)
.onKeyEvent {
val isSpacebarDown = it.key == Key.Spacebar && it.type == KeyEventType.KeyDown
if (isSpacebarDown) onClick()
isSpacebarDown
}
val haloModifier = if (appearance.haloStroke != null)
Modifier.drawBehind {
val outline = appearance.haloShape.createOutline(size, layoutDirection, this)
drawOutline(
outline = outline,
brush = appearance.haloStroke.brush,
style = Stroke(appearance.haloStroke.width.toPx()),
)
}
else
Modifier
val designModifier = Modifier.size(appearance.width, appearance.height)
.shape(appearance.shape, appearance.shapeStroke, appearance.backgroundColor)
.then(haloModifier)
.padding(appearance.symbolPadding)
val baseLine = LocalDensity.current.run { appearance.baseLine.roundToPx() }
val textStyle = appearance.textStyle
content(modifier.then(clickModifier), designModifier, baseLine, checkboxPainter, textStyle, appearance.contentSpacing)
}
@Composable
fun CheckboxRow(
state: ToggleableState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
variation: Any? = null,
content: @Composable RowScope.() -> Unit,
) {
CheckboxImpl(
state,
onClick,
modifier,
enabled,
focusable,
interactionSource,
style,
variation
) { controlModifier, designModifier, baseLine, painter, textStyle, spacing ->
Row(
modifier = controlModifier,
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.CenterVertically
) {
Box(designModifier.alignBy { baseLine }) {
if (painter != null)
Box(Modifier.paint(painter).fillMaxSize())
}
Styles.withTextStyle(LocalTextStyle.current.merge(textStyle)) {
content()
}
}
}
}
@Composable
fun CheckboxRow(
state: MutableState<Boolean>,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
variation: Any? = null,
content: @Composable RowScope.() -> Unit,
) = CheckboxRow(
ToggleableState(state.value),
{ state.value = !state.value },
modifier, enabled, focusable, interactionSource, style, variation, content
)
@Composable
fun Checkbox(
checked: Boolean = false,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
) = Checkbox(
ToggleableState(checked),
{ onCheckedChange(!checked) },
modifier,
enabled, focusable,
interactionSource,
style
)
@Composable
fun Checkbox(
state: MutableState<Boolean>,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
) = Checkbox(
ToggleableState(state.value),
{ state.value = !state.value },
modifier,
enabled, focusable,
interactionSource,
style
)
@Composable
fun Checkbox(
text: String,
checked: Boolean = false,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
) = CheckboxRow(checked, onCheckedChange, modifier, enabled, focusable, interactionSource, style) {
Text(text, Modifier.alignByBaseline())
}
@Composable
fun Checkbox(
text: String,
state: MutableState<Boolean>,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
) = CheckboxRow(state, modifier, enabled, focusable, interactionSource, style) {
Text(text, Modifier.alignByBaseline())
}
@Composable
fun CheckboxRow(
checked: Boolean = false,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
focusable: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
style: CheckboxStyle = LocalCheckboxStyle.current,
variation: Any? = null,
content: @Composable RowScope.() -> Unit,
) = CheckboxRow(
ToggleableState(checked),
{ onCheckedChange(!checked) },
modifier, enabled, focusable,
interactionSource,
style,
variation,
content
)

View File

@@ -0,0 +1,57 @@
package org.jetbrains.jewel.theme.intellij.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.Orientation
import org.jetbrains.jewel.theme.intellij.styles.LocalSeparatorStyle
import org.jetbrains.jewel.theme.intellij.styles.SeparatorStyle
@Composable
fun Separator(
modifier: Modifier = Modifier,
orientation: Orientation = Orientation.Horizontal,
style: SeparatorStyle = LocalSeparatorStyle.current,
indent: Dp = 0.dp
) {
val indentMod = if (indent.value != 0f) {
Modifier.padding(start = indent)
} else {
Modifier
}
val strokeWidth = style.appearance.stroke.width
val orientationModifier = when (orientation) {
Orientation.Horizontal -> Modifier.height(strokeWidth).fillMaxWidth()
Orientation.Vertical -> Modifier.width(strokeWidth).fillMaxHeight()
}
Box(
modifier.then(indentMod)
.then(orientationModifier)
.drawWithContent {
when (orientation) {
Orientation.Horizontal -> {
val start = Offset(0f, strokeWidth.value / 2f)
val end = Offset(size.width, strokeWidth.value / 2f)
drawLine(style.appearance.stroke.brush, start, end, strokeWidth = style.appearance.stroke.width.value)
}
Orientation.Vertical -> {
val start = Offset(strokeWidth.value / 2f, 0f)
val end = Offset(strokeWidth.value / 2f, size.height)
drawLine(style.appearance.stroke.brush, start, end, strokeWidth = style.appearance.stroke.width.value)
}
}
}
)
}

View File

@@ -0,0 +1,95 @@
package org.jetbrains.jewel.theme.intellij.components
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 org.jetbrains.jewel.styles.LocalTextStyle
@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
) {
Text(
AnnotatedString(text),
modifier,
color,
fontSize,
fontStyle,
fontWeight,
fontFamily,
letterSpacing,
textDecoration,
textAlign,
lineHeight,
overflow,
softWrap,
maxLines,
emptyMap(),
onTextLayout,
style
)
}
@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<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
val mergedStyle = style.merge(
TextStyle(
color = color.takeOrElse { style.color },
fontSize = fontSize,
fontWeight = fontWeight,
textAlign = textAlign,
lineHeight = lineHeight,
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
)
)
BasicText(text, modifier, mergedStyle, onTextLayout, overflow, softWrap, maxLines, inlineContent)
}

View File

@@ -0,0 +1,121 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.Insets
import org.jetbrains.jewel.ShapeStroke
import org.jetbrains.jewel.animateShapeStroke
import org.jetbrains.jewel.components.state.AppearanceTransitionState
import org.jetbrains.jewel.components.state.ButtonMouseState
import org.jetbrains.jewel.components.state.ButtonState
import org.jetbrains.jewel.styles.ControlStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.localNotProvided
import org.jetbrains.jewel.theme.intellij.IntelliJMetrics
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
import org.jetbrains.jewel.toBrush
@Immutable
data class ButtonAppearance(
val textStyle: TextStyle = TextStyle.Default, val background: Brush? = null, val shapeStroke: ShapeStroke? = null, val shape: Shape,
val contentPadding: PaddingValues, val minWidth: Dp, val minHeight: Dp,
val haloStroke: ShapeStroke? = null, val haloShape: Shape = shape,
val shadowColor: Color? = null, val shadowElevation: Dp? = null
)
typealias ButtonStyle = ControlStyle<ButtonAppearance, ButtonState>
val LocalButtonStyle = compositionLocalOf<ButtonStyle> { localNotProvided() }
val Styles.button: ButtonStyle
@Composable @ReadOnlyComposable get() = LocalButtonStyle.current
val LocalIconButtonStyle = compositionLocalOf<ButtonStyle> { localNotProvided() }
val Styles.iconButton: ButtonStyle
@Composable @ReadOnlyComposable get() = LocalIconButtonStyle.current
@Composable
fun updateButtonAppearanceTransition(appearance: ButtonAppearance): AppearanceTransitionState {
val transition = updateTransition(appearance)
val background = mutableStateOf(appearance.background)
val shapeStroke = transition.animateShapeStroke(label = "AnimateShapeStroke") { it.shapeStroke }
val haloStroke = transition.animateShapeStroke(label = "AnimateHaloStroke") { it.haloStroke }
return AppearanceTransitionState(background, shapeStroke, haloStroke)
}
enum class IntelliJButtonStyleVariations {
DefaultButton
}
fun ButtonStyle(
palette: IntelliJPalette, metrics: IntelliJMetrics, controlTextStyle: TextStyle
) = ButtonStyle {
val focusHaloStroke = ShapeStroke(metrics.controlFocusHaloWidth, palette.controlFocusHalo.toBrush())
val default = ButtonAppearance(
textStyle = controlTextStyle.copy(palette.button.foreground),
background = palette.button.background,
shape = RoundedCornerShape(metrics.button.arc),
contentPadding = metrics.button.padding,
minWidth = 72.dp,
minHeight = 16.dp,
shapeStroke = ShapeStroke(metrics.button.strokeWidth, palette.button.stroke, Insets(metrics.button.strokeWidth)),
haloStroke = null
)
default {
for (focused in listOf(false, true)) {
val appearance = default.copy(haloStroke = if (focused) focusHaloStroke else null)
populateStates(appearance, focused, focusHaloStroke, controlTextStyle, palette, metrics)
}
}
variation(IntelliJButtonStyleVariations.DefaultButton) {
for (focused in listOf(false, true)) {
val strokeColor = if (focused) palette.button.defaultStrokeFocused.toBrush() else palette.button.defaultStroke
val appearance = default.copy(
background = palette.button.defaultBackground,
textStyle = controlTextStyle.copy(color = palette.button.defaultForeground),
shapeStroke = ShapeStroke(metrics.button.strokeWidth, strokeColor, Insets(metrics.button.strokeWidth)),
haloStroke = if (focused) focusHaloStroke else null,
)
populateStates(appearance, focused, focusHaloStroke, controlTextStyle, palette, metrics)
}
}
}
private fun ControlStyle.ControlVariationBuilder<ButtonAppearance, ButtonState>.populateStates(
appearance: ButtonAppearance,
focused: Boolean,
focusHaloStroke: ShapeStroke,
controlTextStyle: TextStyle,
palette: IntelliJPalette,
metrics: IntelliJMetrics
) {
state(ButtonState(focused = focused), appearance)
state(ButtonState(ButtonMouseState.Pressed, focused = focused), appearance.copy(haloStroke = focusHaloStroke))
state(ButtonState(ButtonMouseState.Hovered, focused = focused), appearance)
state(
ButtonState(enabled = false, focused = focused),
appearance.copy(
textStyle = controlTextStyle.copy(palette.button.foregroundDisabled),
background = Color.Transparent.toBrush(),
shapeStroke = ShapeStroke(metrics.button.strokeWidth, palette.controlStrokeDisabled.toBrush(), Insets(metrics.button.strokeWidth))
)
)
}

View File

@@ -0,0 +1,107 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.ShapeStroke
import org.jetbrains.jewel.components.state.ButtonMouseState
import org.jetbrains.jewel.components.state.CheckboxState
import org.jetbrains.jewel.styles.ControlStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.localNotProvided
import org.jetbrains.jewel.theme.intellij.IntelliJPainters
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
import org.jetbrains.jewel.toBrush
typealias CheckboxStyle = ControlStyle<CheckboxAppearance, CheckboxState>
@Immutable
data class CheckboxAppearance(
val textStyle: TextStyle = TextStyle.Default,
val width: Dp = 16.dp,
val height: Dp = 16.dp,
val contentSpacing: Dp = 8.dp,
val backgroundColor: Color = Color.Blue,
val shapeStroke: ShapeStroke? = ShapeStroke(1.dp, Color.Blue.toBrush()),
val shape: Shape = RectangleShape,
val interiorPainter: (@Composable () -> Painter)? = null,
val symbolPadding: Dp = 2.dp,
val baseLine: Dp = 14.dp,
val haloStroke: ShapeStroke? = null,
val haloShape: Shape = shape,
)
val LocalCheckboxStyle = compositionLocalOf<CheckboxStyle> { localNotProvided() }
val Styles.checkbox: CheckboxStyle
@Composable
@ReadOnlyComposable
get() = LocalCheckboxStyle.current
fun CheckboxStyle(
palette: IntelliJPalette,
painters: IntelliJPainters,
controlTextStyle: TextStyle
) = CheckboxStyle {
default {
for (enabled in listOf(false, true)) {
for (focused in listOf(false, true)) {
for (toggleableState in listOf(ToggleableState.On, ToggleableState.Indeterminate, ToggleableState.Off)) {
val (painter, textStyle) = if (enabled) {
if (focused) {
when (toggleableState) {
ToggleableState.On -> painters.checkbox.selectedFocused
ToggleableState.Indeterminate -> painters.checkbox.indeterminateFocused
ToggleableState.Off -> painters.checkbox.unselectedFocused
} to controlTextStyle.copy(color = palette.text)
} else {
when (toggleableState) {
ToggleableState.On -> painters.checkbox.selected
ToggleableState.Indeterminate -> painters.checkbox.indeterminate
ToggleableState.Off -> painters.checkbox.unselected
} to controlTextStyle.copy(color = palette.text)
}
} else {
when (toggleableState) {
ToggleableState.On -> painters.checkbox.selectedDisabled
ToggleableState.Indeterminate -> painters.checkbox.indeterminateDisabled
ToggleableState.Off -> painters.checkbox.unselectedDisabled
} to controlTextStyle.copy(color = palette.textDisabled)
}
ButtonMouseState.values().forEach { buttonState ->
state(
CheckboxState(
toggleableState,
buttonState,
enabled = enabled,
focused = focused
),
CheckboxAppearance(
interiorPainter = painter,
backgroundColor = Color.Transparent,
symbolPadding = 0.dp,
shapeStroke = null,
width = 19.dp,
height = 19.dp,
textStyle = textStyle
)
)
}
}
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.styles.ControlStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.localNotProvided
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
typealias FrameStyle = ControlStyle<FrameAppearance, Unit>
@Immutable
data class FrameAppearance(
val backgroundColor: Color = Color.White,
)
val LocalFrameStyle = compositionLocalOf<FrameStyle> { localNotProvided() }
val Styles.frame: FrameStyle
@Composable
@ReadOnlyComposable
get() = LocalFrameStyle.current
fun FrameStyle(palette: IntelliJPalette) = FrameStyle {
default {
state(Unit, FrameAppearance(backgroundColor = palette.background))
}
}

View File

@@ -0,0 +1,109 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.ShapeStroke
import org.jetbrains.jewel.components.state.ButtonMouseState
import org.jetbrains.jewel.styles.ControlStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.localNotProvided
import org.jetbrains.jewel.theme.intellij.IntelliJPainters
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
import org.jetbrains.jewel.toBrush
typealias RadioButtonStyle = ControlStyle<RadioButtonAppearance, RadioButtonState>
data class RadioButtonState(
val checked: Boolean,
val mouse: ButtonMouseState = ButtonMouseState.None,
val enabled: Boolean = true,
val focused: Boolean = false,
)
@Immutable
data class RadioButtonAppearance(
val textStyle: TextStyle = TextStyle.Default,
val width: Dp = 16.dp,
val height: Dp = 16.dp,
val contentSpacing: Dp = 8.dp,
val backgroundColor: Color = Color.Blue,
val shapeStroke: ShapeStroke? = ShapeStroke(1.dp, Color.Blue.toBrush()),
val shape: Shape = RectangleShape,
val interiorPainter: (@Composable () -> Painter)? = null,
val symbolPadding: Dp = 2.dp,
val baseLine: Dp = 14.dp,
val haloStroke: ShapeStroke? = null,
val haloShape: Shape = shape,
)
val LocalRadioButtonStyle = compositionLocalOf<RadioButtonStyle> { localNotProvided() }
val Styles.radioButton: RadioButtonStyle
@Composable
@ReadOnlyComposable
get() = LocalRadioButtonStyle.current
fun RadioButtonStyle(
palette: IntelliJPalette,
painters: IntelliJPainters,
controlTextStyle: TextStyle
) = RadioButtonStyle {
default {
for (enabled in listOf(false, true)) {
for (focused in listOf(false, true)) {
for (checked in listOf(false, true)) {
val (painter, textStyle) = if (enabled) {
if (focused) {
when (checked) {
true -> painters.radioButton.selectedFocused
false -> painters.radioButton.unselectedFocused
} to controlTextStyle.copy(color = palette.text)
} else {
when (checked) {
true -> painters.radioButton.selected
false -> painters.radioButton.unselected
} to controlTextStyle.copy(color = palette.text)
}
} else {
when (checked) {
true -> painters.radioButton.selectedDisabled
false -> painters.radioButton.unselectedDisabled
} to controlTextStyle.copy(color = palette.textDisabled)
}
ButtonMouseState.values().forEach { buttonState ->
state(
RadioButtonState(
checked,
buttonState,
enabled = enabled,
focused = focused
),
RadioButtonAppearance(
textStyle = textStyle,
interiorPainter = painter,
backgroundColor = Color.Transparent,
symbolPadding = 0.dp,
shapeStroke = null,
width = 19.dp,
height = 19.dp
)
)
}
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.foundation.ScrollbarStyle
import androidx.compose.foundation.shape.RoundedCornerShape
import org.jetbrains.jewel.theme.intellij.IntelliJMetrics
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
// TODO consider that scrollbars have different behaviors on different OSes
// Scrollbars on IJ do NOT follow the LaF defaults! They are only influenced by the ScrollbarUI being used, which depends on the OS.
//
// * On Win and Linux, it's pretty easy: follow what DefaultScrollbarUI does
// * Except that you have different behavior if com.intellij.ui.components.ScrollSettings.isThumbSmallIfOpaque == true
// * On Mac, it follows the OS setting (legacy vs overlay)
// * This involves JNI calls to get that setting, and to observe its changes (see com.intellij.ui.components.MacScrollBarUI.Style)
// * The style is somewhat different depending on the style of scrollbars
//
// The standard Compose ScrollbarStyle lacks a lot of things, too, such as the ability to paint the track when needed,
// and things like thumb borders, etc.
fun ScrollbarStyle(palette: IntelliJPalette, metrics: IntelliJMetrics) = ScrollbarStyle(
minimalHeight = metrics.scrollbar.minSize,
thickness = metrics.scrollbar.thickness,
shape = RoundedCornerShape(metrics.scrollbar.thumbCornerSize),
hoverDurationMillis = 11 * 16, // See com.intellij.ui.components.ScrollBarPainter.ScrollBarPainter: 11 frames, assuming 60fps (16 ms/f)
unhoverColor = palette.scrollbar.thumbIdleColor,
hoverColor = palette.scrollbar.thumbHoverColor
)

View File

@@ -0,0 +1,35 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.foundation.BorderStroke
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.theme.intellij.IntelliJMetrics
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
@Immutable
data class SeparatorStyle(
val appearance: SeparatorAppearance = SeparatorAppearance(),
)
data class SeparatorAppearance(
val background: Color = Color.Unspecified,
val stroke: BorderStroke = BorderStroke(1.dp, Color(0xFFD1D1D1)),
)
val LocalSeparatorStyle = compositionLocalOf { SeparatorStyle() }
val Styles.separator: SeparatorStyle
@Composable
@ReadOnlyComposable
get() = LocalSeparatorStyle.current
fun SeparatorStyle(palette: IntelliJPalette, metrics: IntelliJMetrics): SeparatorStyle = SeparatorStyle(
appearance = SeparatorAppearance(
background = palette.separator.background,
stroke = BorderStroke(metrics.separator.strokeWidth, palette.separator.color)
)
)

View File

@@ -0,0 +1,210 @@
package org.jetbrains.jewel.theme.intellij.styles
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
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.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.Insets
import org.jetbrains.jewel.ShapeStroke
import org.jetbrains.jewel.components.state.TextFieldState
import org.jetbrains.jewel.styles.ControlStyle
import org.jetbrains.jewel.styles.Styles
import org.jetbrains.jewel.styles.localNotProvided
import org.jetbrains.jewel.theme.intellij.IntelliJMetrics
import org.jetbrains.jewel.theme.intellij.IntelliJPalette
import org.jetbrains.jewel.toBrush
typealias TextFieldStyle = ControlStyle<TextFieldAppearance, TextFieldState>
data class TextFieldAppearance(
val textStyle: TextStyle = TextStyle.Default,
val backgroundColor: Color,
val shapeStroke: ShapeStroke? = null,
val shape: Shape,
val adornmentStroke: ShapeStroke? = null,
val adornmentShape: Shape? = null,
val cursorBrush: Brush = SolidColor(Color.Black),
val contentPadding: PaddingValues,
val haloStroke: ShapeStroke? = null,
val haloShape: Shape = shape,
val minWidth: Dp = Dp.Unspecified,
val minHeight: Dp = Dp.Unspecified,
)
val LocalTextFieldStyle = compositionLocalOf<TextFieldStyle> { localNotProvided() }
val Styles.textField: TextFieldStyle
@Composable
@ReadOnlyComposable
get() = LocalTextFieldStyle.current
fun TextFieldStyle(
palette: IntelliJPalette,
metrics: IntelliJMetrics,
textStyle: TextStyle
) = TextFieldStyle {
val defaultAppearance = TextFieldAppearance(
textStyle = textStyle.copy(palette.textField.foreground),
backgroundColor = palette.textField.background,
shape = RectangleShape,
contentPadding = PaddingValues(10.dp, 7.dp),
cursorBrush = palette.text.toBrush(),
shapeStroke = ShapeStroke(
1.dp,
palette.controlStroke.toBrush(),
Insets(1.dp)
),
haloShape = RoundedCornerShape(metrics.controlFocusHaloArc),
minWidth = 8.dp * 8,
minHeight = 8.dp * 2,
)
val disabledAppearance = defaultAppearance.copy(
textStyle = defaultAppearance.textStyle.copy(color = palette.textField.foregroundDisabled),
backgroundColor = palette.textField.backgroundDisabled
)
val focusedAppearance = defaultAppearance.copy(
shapeStroke = ShapeStroke(
1.dp,
palette.controlStrokeFocused.toBrush(),
Insets(1.dp)
),
haloStroke = ShapeStroke(
metrics.controlFocusHaloWidth,
palette.controlFocusHalo.toBrush(),
Insets((-1).dp)
)
)
default {
allStateCombinations { enabled, focused, hovered ->
val appearance = when {
enabled -> when {
focused -> focusedAppearance
else -> defaultAppearance
}
else -> disabledAppearance
}
state(
TextFieldState(
focused = focused,
hovered = hovered,
enabled = enabled
),
appearance
)
}
}
variation(IntelliJTextFieldVariations.Error) {
allStateCombinations { enabled, focused, hovered ->
val appearance = if (enabled) {
defaultAppearance.copy(
shapeStroke = ShapeStroke(
1.dp,
palette.controlHaloError.toBrush(),
Insets(1.dp)
),
haloStroke = ShapeStroke(
metrics.controlFocusHaloWidth,
palette.controlInactiveHaloError.toBrush(),
Insets((-1).dp)
)
)
} else {
disabledAppearance
}
state(
TextFieldState(
focused = focused,
hovered = hovered,
enabled = enabled
),
appearance
)
}
}
variation(IntelliJTextFieldVariations.Warning) {
allStateCombinations { enabled, focused, hovered ->
val appearance = when {
enabled -> defaultAppearance.copy(
shapeStroke = ShapeStroke(
1.dp,
palette.controlHaloWarning.toBrush(),
Insets(1.dp)
),
haloStroke = ShapeStroke(
metrics.controlFocusHaloWidth,
palette.controlInactiveHaloWarning.toBrush(),
Insets((-1).dp)
)
)
else -> disabledAppearance
}
state(
TextFieldState(
focused = focused,
hovered = hovered,
enabled = enabled
),
appearance
)
}
}
variation(IntelliJTextFieldVariations.Search) {
allStateCombinations { enabled, focused, hovered ->
val appearance = when {
enabled -> when {
focused -> focusedAppearance.copy(shape = RoundedCornerShape(metrics.controlArc))
else -> defaultAppearance.copy(shape = RoundedCornerShape(metrics.controlArc))
}
else -> disabledAppearance.copy(shape = RoundedCornerShape(metrics.controlArc))
}
state(
TextFieldState(
focused = focused,
hovered = hovered,
enabled = enabled
),
appearance
)
}
}
}
private fun ControlStyle.ControlVariationBuilder<TextFieldAppearance, TextFieldState>.allStateCombinations(
action: ControlStyle.ControlVariationBuilder<TextFieldAppearance, TextFieldState>.(enabled: Boolean, focused: Boolean, hovered: Boolean) -> Unit
) {
for (enabled in listOf(false, true)) {
for (focused in listOf(false, true)) {
for (hovered in listOf(false, true)) {
action(enabled, focused, hovered)
}
}
}
}
enum class IntelliJTextFieldVariations {
Error,
Search,
Warning
}

View File

@@ -0,0 +1,22 @@
plugins {
id(libs.plugins.kotlinJvm.get().pluginId)
id(libs.plugins.composeDesktop.get().pluginId)
}
kotlin {
target {
compilations.all {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
}
}
dependencies {
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
api(projects.themes.intellij)
}

View File

@@ -0,0 +1,23 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.runtime.Composable
@Composable
fun IntelliJThemeLight(content: @Composable () -> Unit) =
IntelliJTheme(
IntelliJPalette.light,
IntelliJMetrics.default,
IntelliJPainters.light,
IntelliJTypography.default,
content
)
@Composable
fun IntelliJThemeDark(content: @Composable () -> Unit) =
IntelliJTheme(
IntelliJPalette.darcula,
IntelliJMetrics.default,
IntelliJPainters.darcula,
IntelliJTypography.default,
content
)

View File

@@ -0,0 +1,43 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.isMacOs
val IntelliJMetrics.Button.Companion.default
get() = IntelliJMetrics.Button(
strokeWidth = 1.dp,
arc = CornerSize(6.dp),
padding = PaddingValues(horizontal = 14.dp, vertical = 2.dp)
)
val IntelliJMetrics.Companion.default
get() = IntelliJMetrics(
gridSize = 8.dp,
singlePadding = 8.dp,
doublePadding = 16.dp,
controlFocusHaloWidth = 2.dp,
controlFocusHaloArc = 1.dp,
controlArc = 3.dp,
button = IntelliJMetrics.Button.default,
separator = IntelliJMetrics.Separator.default,
scrollbar = if (isMacOs()) IntelliJMetrics.Scrollbar.macOs else IntelliJMetrics.Scrollbar.default
)
val IntelliJMetrics.Scrollbar.Companion.default
get() = IntelliJMetrics.Scrollbar(
minSize = 13.dp, // myThickness * 2 (see DefaultScrollBarUI.updateThumbBounds)
thickness = 13.dp, // myThickness
thumbCornerSize = CornerSize(0.dp), // See com.intellij.ui.components.ScrollBarPainter.Thumb.paint
)
val IntelliJMetrics.Scrollbar.Companion.macOs
get() = IntelliJMetrics.Scrollbar(
minSize = 13.dp, // myThickness * 2 (see DefaultScrollBarUI.updateThumbBounds)
thickness = 14.dp, // myThickness
thumbCornerSize = CornerSize(14.dp), // See com.intellij.ui.components.ScrollBarPainter.Thumb.paint
)
val IntelliJMetrics.Separator.Companion.default
get() = IntelliJMetrics.Separator(strokeWidth = 1.dp)

View File

@@ -0,0 +1,148 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.isMacOs
import org.jetbrains.jewel.toBrush
val IntelliJPalette.Checkbox.Companion.light
get() = IntelliJPalette.Checkbox(
background = Color(0xFFF2F2F2),
foreground = Color(0xFF000000),
foregroundDisabled = Color(0xFF8C8C8C),
)
val IntelliJPalette.Checkbox.Companion.darcula
get() = IntelliJPalette.Checkbox(
background = Color(0xFF3C3F41),
foreground = Color(0xFFBBBBBB),
foregroundDisabled = Color(0xFF999999),
)
val IntelliJPalette.RadioButton.Companion.light
get() = IntelliJPalette.RadioButton(
background = Color(0xFFF2F2F2),
foreground = Color(0xFF000000),
foregroundDisabled = Color(0xFF8C8C8C),
)
val IntelliJPalette.RadioButton.Companion.darcula
get() = IntelliJPalette.RadioButton(
background = Color(0xFF3C3F41),
foreground = Color(0xFFBBBBBB),
foregroundDisabled = Color(0xFF999999),
)
val IntelliJPalette.TextField.Companion.light
get() = IntelliJPalette.TextField(
background = Color(0xFFFFFFFF),
backgroundDisabled = Color(0xFFF2F2F2),
foreground = Color(0xFF000000),
foregroundDisabled = Color(0xFF8C8C8C)
)
val IntelliJPalette.TextField.Companion.darcula
get() = IntelliJPalette.TextField(
background = Color(0xFF45494A),
backgroundDisabled = Color(0xFF3C3F41),
foreground = Color(0xFFBBBBBB),
foregroundDisabled = Color(0xFF777777)
)
val IntelliJPalette.Button.Companion.light
get() = IntelliJPalette.Button(
background = Color(0xFFFFFFFF).toBrush(),
foreground = Color.Black,
foregroundDisabled = Color(0xFF8C8C8C),
shadow = Color(0x00A6A6A6),
stroke = Color(0XFFC4C4C4).toBrush(),
strokeFocused = Color(0xFF87AFDA),
strokeDisabled = Color(0xFFCFCFCF),
defaultBackground = Brush.verticalGradient(listOf(Color(0xFF528CC7), Color(0xFF4989CC))),
defaultForeground = Color.White,
defaultStroke = Color(0xFF487EB8).toBrush(), //Brush.verticalGradient(listOf(Color(0xFF487EB8), Color(0xFF346DAD))),
defaultStrokeFocused = Color(0xFFA9C9F5),
defaultShadow = Color(0x00A6A6A6)
)
val IntelliJPalette.Button.Companion.darcula
get() = IntelliJPalette.Button(
background = Color(0xFF4C5052).toBrush(),
foreground = Color(0xFFBBBBBB),
foregroundDisabled = Color(0xFF777777),
shadow = Color(0xFF999999),
stroke = Color(0XFF5E6060).toBrush(),
strokeFocused = Color(0xFF466D94),
strokeDisabled = Color(0xFF5E6060),
defaultBackground = Color(0xFF365880).toBrush(),
defaultForeground = Color(0xFFBBBBBB),
defaultStroke = Color(0xFF4C708C).toBrush(),
defaultStrokeFocused = Color(0xFFA9C9F5),
defaultShadow = Color.Unspecified
)
val IntelliJPalette.Separator.Companion.light
get() = IntelliJPalette.Separator(
color = Color(0xFFD1D1D1),
background = Color.Unspecified,
)
val IntelliJPalette.Separator.Companion.darcula
get() = IntelliJPalette.Separator(
color = Color(0xFF3C3F41),
background = Color.Unspecified,
)
val IntelliJPalette.Scrollbar.Companion.light
get() = IntelliJPalette.Scrollbar(
thumbIdleColor = if (isMacOs()) Color(0x00000000) else Color(0x33737373),
thumbHoverColor = if (isMacOs()) Color(0x80000000) else Color(0x47737373),
)
val IntelliJPalette.Scrollbar.Companion.darcula
get() = IntelliJPalette.Scrollbar(
thumbIdleColor = if (isMacOs()) Color(0x00808080) else Color(0x47A6A6A6),
thumbHoverColor = if (isMacOs()) Color(0x8C808080) else Color(0x59A6A6A6),
)
val IntelliJPalette.Companion.light
get() = IntelliJPalette(
button = IntelliJPalette.Button.light,
checkbox = IntelliJPalette.Checkbox.light,
radioButton = IntelliJPalette.RadioButton.light,
textField = IntelliJPalette.TextField.light,
background = Color(0xFFF2F2F2),
text = Color.Black,
textDisabled = Color(0xFF8C8C8C),
controlStroke = Color(0xFFC4C4C4),
controlStrokeDisabled = Color(0xFFCFCFCF),
controlStrokeFocused = Color(0XFF87AFDA), // Component.focusedBorderColor
controlFocusHalo = Color(0XFF97C3F3),
controlInactiveHaloError = Color(0XFFEBBCBC),
controlInactiveHaloWarning = Color(0XFFFFD385),
controlHaloError = Color(0XFFE53E4D),
controlHaloWarning = Color(0XFFE2A53A),
separator = IntelliJPalette.Separator.light,
scrollbar = IntelliJPalette.Scrollbar.light
)
val IntelliJPalette.Companion.darcula
get() = IntelliJPalette(
button = IntelliJPalette.Button.darcula,
checkbox = IntelliJPalette.Checkbox.darcula,
textField = IntelliJPalette.TextField.darcula,
radioButton = IntelliJPalette.RadioButton.darcula,
background = Color(0xFF3C3F41),
text = Color(0xFFBBBBBB),
textDisabled = Color(0xFF777777),
controlStroke = Color(0xFF646464),
controlStrokeDisabled = Color(0xFF646464),
controlStrokeFocused = Color(0XFF466D94),
controlFocusHalo = Color(0XFF3D6185),
controlInactiveHaloError = Color(0XFF725252),
controlInactiveHaloWarning = Color(0XFF6E5324),
controlHaloError = Color(0XFF8B3C3C),
controlHaloWarning = Color(0XFFAC7920),
separator = IntelliJPalette.Separator.darcula,
scrollbar = IntelliJPalette.Scrollbar.darcula,
)

View File

@@ -0,0 +1,12 @@
package org.jetbrains.jewel.theme.intellij
import androidx.compose.ui.text.TextStyle
val IntelliJTypography.Companion.default
get() = IntelliJTypography(
default = TextStyle.Default,
button = TextStyle.Default,
checkBox = TextStyle.Default,
radioButton = TextStyle.Default,
textField = TextStyle.Default
)

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="none" fill-rule="evenodd">
<rect width="14" height="14" x="3" y="2" fill="#43494A" rx="2"/>
<path fill="#6B6B6B"
d="M5,2 L15,2 C16.1045695,2 17,2.8954305 17,4 L17,14 C17,15.1045695 16.1045695,16 15,16 L5,16 C3.8954305,16 3,15.1045695 3,14 L3,4 C3,2.8954305 3.8954305,2 5,2 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 584 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="none" fill-rule="evenodd">
<rect width="14" height="14" x="3" y="2" fill="#3C3F41" rx="2"/>
<path fill="#545556"
d="M5,2 L15,2 C16.1045695,2 17,2.8954305 17,4 L17,14 C17,15.1045695 16.1045695,16 15,16 L5,16 C3.8954305,16 3,15.1045695 3,14 L3,4 C3,2.8954305 3.8954305,2 5,2 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 584 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="none" fill-rule="evenodd">
<rect width="14" height="14" x="3" y="2" fill="#43494A"/>
<path fill="#3D6185"
d="M5,0 L15,0 C17.209139,-4.05812251e-16 19,1.790861 19,4 L19,14 C19,16.209139 17.209139,18 15,18 L5,18 C2.790861,18 1,16.209139 1,14 L1,4 C1,1.790861 2.790861,4.05812251e-16 5,0 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"/>
<path fill="#466D94"
d="M5,2 L15,2 C16.1045695,2 17,2.8954305 17,4 L17,14 C17,15.1045695 16.1045695,16 15,16 L5,16 C3.8954305,16 3,15.1045695 3,14 L3,4 C3,2.8954305 3.8954305,2 5,2 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"
opacity=".65"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 995 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="none" fill-rule="evenodd">
<rect width="14" height="14" x="3" y="2" fill="#43494A" rx="2"/>
<path fill="#6B6B6B"
d="M5,2 L15,2 C16.1045695,2 17,2.8954305 17,4 L17,14 C17,15.1045695 16.1045695,16 15,16 L5,16 C3.8954305,16 3,15.1045695 3,14 L3,4 C3,2.8954305 3.8954305,2 5,2 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"/>
<rect width="8.4" height="2.5" x="5.737" y="7.73" fill="#A7A7A7" rx="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="none" fill-rule="evenodd">
<rect width="14" height="14" x="3" y="2" fill="#3C3F41" rx="2"/>
<path fill="#545556"
d="M5,2 L15,2 C16.1045695,2 17,2.8954305 17,4 L17,14 C17,15.1045695 16.1045695,16 15,16 L5,16 C3.8954305,16 3,15.1045695 3,14 L3,4 C3,2.8954305 3.8954305,2 5,2 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"/>
<rect width="8.4" height="2.5" x="5.737" y="7.73" fill="#606060" rx="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="none" fill-rule="evenodd">
<rect width="14" height="14" x="3" y="2" fill="#43494A" rx="2"/>
<path fill="#3D6185"
d="M5,0 L15,0 C17.209139,-4.05812251e-16 19,1.790861 19,4 L19,14 C19,16.209139 17.209139,18 15,18 L5,18 C2.790861,18 1,16.209139 1,14 L1,4 C1,1.790861 2.790861,4.05812251e-16 5,0 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"/>
<path fill="#466D94"
d="M5,2 L15,2 C16.1045695,2 17,2.8954305 17,4 L17,14 C17,15.1045695 16.1045695,16 15,16 L5,16 C3.8954305,16 3,15.1045695 3,14 L3,4 C3,2.8954305 3.8954305,2 5,2 Z M5,3 C4.44771525,3 4,3.44771525 4,4 L4,14 C4,14.5522847 4.44771525,15 5,15 L15,15 C15.5522847,15 16,14.5522847 16,14 L16,4 C16,3.44771525 15.5522847,3 15,3 L5,3 Z"
opacity=".65"/>
<rect width="8.4" height="2.5" x="5.737" y="7.73" fill="#A7A7A7" rx="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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