Initial commit
GitOrigin-RevId: e4b68ae8dc2075cd6efe46842e20567b05577083
96
platform/jewel/.gitignore
vendored
Normal 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
|
||||
48
platform/jewel/.idea/codeStyles/Project.xml
generated
Normal 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>
|
||||
5
platform/jewel/.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
@@ -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 |
23
platform/jewel/.idea/runConfigurations/IDE_sample.xml
generated
Normal 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>
|
||||
23
platform/jewel/.idea/runConfigurations/Stand_alone_sample.xml
generated
Normal 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
@@ -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
@@ -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.
|
||||
7
platform/jewel/art/jewel-logo.svg
Normal 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 |
17
platform/jewel/build.gradle.kts
Normal 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")
|
||||
}
|
||||
}
|
||||
20
platform/jewel/gradle/libs.versions.toml
Normal 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" }
|
||||
BIN
platform/jewel/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
platform/jewel/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
|
||||
40
platform/jewel/library/build.gradle.kts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.jetbrains.jewel
|
||||
|
||||
enum class Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 }
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.jetbrains.jewel.components.state
|
||||
|
||||
enum class TabState {
|
||||
Normal,
|
||||
Selected,
|
||||
Hovered,
|
||||
Disabled,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
38
platform/jewel/sample/build.gradle.kts
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
19
platform/jewel/settings.gradle.kts
Normal 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"
|
||||
)
|
||||
22
platform/jewel/themes/intellij/build.gradle.kts
Normal 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)
|
||||
}
|
||||
40
platform/jewel/themes/intellij/idea/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
22
platform/jewel/themes/intellij/standalone/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |