Create IDE LaF bridge (#102)

* [WIP] Start building the bridge

* Update dependencies, remove unused ones

* [WIP] Continue building the bridge

* [WIP] Continue building the bridge (got to Chip)

Includes tweaks on wrong values etc

* [WIP] Fill out infra for IntUiBridge.kt

Still not done!

* [WIP] Fill out infra for IntUiBridge.kt

Still not done!

* [WIP] More work

* [WIP] More progress (missing text fields & tabs)

* Finish filling in the bridge, then 🤞

* Make default fallbacks explicit, fix some issues

* Delete unused code

* Implement New UI icon mapping support in Bridge

Note: it doesn't work yet, since IconMapLoader.loadIconMapping()
complains that the mapping isn't done (when it is, since it's done in
the IDE pre-loading).

I suspect this is a classloading issue.

* More cleanup, stuff's still broken yey

* More fixin' stuff, hopefully the CI too

* Fix CI warning

* Refactor UI Component Styling in Jewel to Support defaults (#104)

* Refactor UI Component Styling in Jewel to Support defaults

Retrieving text styles requires coroutines. This commit aims to allow the theme to start with text defaults and as soon as text styles get retrieved, the theme gets updated.

* Fixes according to MR comments

* Fix this thing

---------

Co-authored-by: Sebastiano Poggi <sebp@google.com>

* More fixes to components/theme, cleanups

* Improve showcase and simplify text area (bye hints)

* Improve showcase and simplify text area (bye hints), fix more things

* Avoid crashing on old UI

---------

Co-authored-by: Lamberto Basti <basti.lamberto@gmail.com>
GitOrigin-RevId: b1d9fa9ec52c64833cd14a453c524195eb8932bd
This commit is contained in:
Sebastiano Poggi
2023-09-01 17:10:42 +02:00
committed by intellij-monorepo-bot
parent 274830138a
commit f3dd498338
144 changed files with 4466 additions and 2685 deletions

View File

@@ -39,13 +39,15 @@ jobs:
run: chmod +x gradlew
- name: Run :check task
run: ./gradlew check
run: ./gradlew check --continue
- name: Merge SARIF reports
# Necessary because upload-sarif only takes up to 15 SARIF files and we have more
run: ./gradlew :mergeSarifReports
if: ${{ always() }}
- uses: github/codeql-action/upload-sarif@v2
if: ${{ always() }}
with:
sarif_file: ${{ github.workspace }}/build/reports/static-analysis.sarif
checkout_path: ${{ github.workspace }}

View File

@@ -19,6 +19,9 @@
<option name="IF_RPAREN_ON_NEW_LINE" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="TAB_SIZE" value="2" />

File diff suppressed because it is too large Load Diff

View File

@@ -1,315 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ActionIsNotPreviewFriendly" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintAssertionSideEffect" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintBadHostnameVerifier" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintFileEndsWithExt" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintGradleDeprecatedConfiguration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintGradlePath" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintJcenterRepositoryObsolete" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintLocalSuppress" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="AndroidLintMissingSuperCall" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="AndroidLintNotConstructor" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintNotInterpolated" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="AndroidLintRange" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="AndroidLintSQLiteString" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintSuspiciousIndentation" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="AndroidLintUseValueOf" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintUsingHttp" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AntMissingPropertiesFileInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="ArrayEquals" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ArrayInDataClass" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="AssertBetweenInconvertibleTypes" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AssertWithSideEffects" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AssertionCanBeIf" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="AssignmentToCatchBlockParameter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AtomicFieldUpdaterIssues" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="BooleanConstructor" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="BooleanMethodIsAlwaysInverted" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="BoxingBoxedValue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CanBeFinal" enabled="false" level="WARNING" enabled_by_default="false">
<option name="REPORT_CLASSES" value="false" />
<option name="REPORT_METHODS" value="false" />
<option name="REPORT_FIELDS" value="true" />
</inspection_tool>
<inspection_tool class="CanBeParameter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CanBePrimaryConstructorProperty" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CascadeIf" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="CastCanBeReplacedWithVariable" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="CheckDtdRefs" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="CloneableImplementsClone" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CollectionContainsUrl" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ComparatorMethodParameterNotUsed" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ComparatorResultComparison" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ComponentNotRegistered" enabled="false" level="WARNING" enabled_by_default="false">
<option name="CHECK_ACTIONS" value="true" />
<option name="IGNORE_NON_PUBLIC" value="true" />
</inspection_tool>
<inspection_tool class="ComponentRegistrationProblems" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="ConditionCoveredByFurtherCondition" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ConstantConditionIf" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ConstantConditionalExpression" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ControlFlowWithEmptyBody" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Convert2MethodRef" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ConvertLambdaToReference" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ConvertReferenceToLambda" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ConvertTwoComparisonsToRangeCheck" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="CopyWithoutNamedArguments" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="DataClassPrivateConstructor" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Dependency" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="DeprecatedClassUsageInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DeprecatedMavenDependency" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DoubleBraceInitialization" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="DuplicateExpressions" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigCharClassRedundancy" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigEncoding" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigListAcceptability" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="EditorConfigNoMatchingFiles" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigOptionRedundancy" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigShadowedOption" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigShadowingOption" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EditorConfigValueUniqueness" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="EditorConfigWildcardRedundancy" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EmptyFinallyBlock" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EmptyRange" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EmptyStatementBody" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EndlessStream" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EnhancedSwitchBackwardMigration" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="EnhancedSwitchMigration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EnumSwitchStatementWhichMissesCases" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="EqualsBetweenInconvertibleTypes" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EqualsHashCodeCalledOnUrl" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EqualsWithItself" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ExcessiveRangeCheck" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ExplicitArgumentCanBeLambda" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ExpressionMayBeFactorized" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ExtendsAnnotation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FakeJvmFieldConstant" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FinalStaticMethod" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FinalizeNotProtected" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FinallyBlockCannotCompleteNormally" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FoldExpressionIntoStream" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="FoldInitializerAndIfToElvis" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ForeignDelegate" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="FuseStreamOperations" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrMethodMayBeStatic" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrReassignedInClosureLocalVar" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrSwitchExhaustivenessCheck" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="GrUnnecessaryPublicModifier" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GradlePackageUpdate" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="GroovyAssignmentCanBeOperatorAssignment" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="GroovyConditionalCanBeElvis" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="GroovyConditionalWithIdenticalBranches" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyConstantConditional" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyConstructorNamedArguments" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyDoubleNegation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyDuplicateSwitchBranch" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyEmptyStatementBody" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyInfiniteLoopStatement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyLabeledStatement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyResultOfObjectAllocationIgnored" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovySynchronizationOnNonFinalField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyTrivialIf" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyUnusedAssignment" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GroovyVariableNotAssigned" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HardwiredNamespacePrefix" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownBooleanAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IOStreamConstructor" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IgnoreRelativeEntry" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="IncorrectParentDisposable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IndexZeroUsage" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="InfiniteRecursion" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="InjectionNotApplicable" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="JUnit5AssertionsConverter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Java8MapApi" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Java9ModuleExportsPackageToItself" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavaCollectionsStaticMethodOnImmutableList" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavaFxEventHandler" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavaLangInvokeHandleSignature" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JavaStylePropertiesInvocation" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="JavacQuirks" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JoinDeclarationAndAssignment" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JsonPathEvaluateUnknownKey" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JsonSchemaRefReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Junit4Converter" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="KDocUnresolvedReference" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="KotlinDeprecation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="KotlinInternalInJava" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="KotlinJvmAnnotationInJava" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="KotlinPlaceholderCountMatchesArgumentCount" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="KotlinThrowableNotThrown" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LambdaCanBeMethodCall" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="LambdaCanBeReplacedWithAnonymous" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LeakableMapKey" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LeakingThis" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LiftReturnOrAssignment" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="LocalVariableName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="Lombok" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MainFunctionReturnUnit" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MalformedFormatString" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ManualMinMaxCalculation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownIncorrectTableFormatting" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownIncorrectlyNumberedListItem" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownLinkDestinationWithSpaces" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MarkdownNoTableBorders" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="MeaninglessRecordAnnotationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MethodRefCanBeReplacedWithLambda" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="MigrateDiagnosticSuppression" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MismatchedCollectionQueryUpdate" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MismatchedJavadocCode" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MismatchedStringBuilderQueryUpdate" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MissingOverrideAnnotation" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="MissingRecentApi" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="MissingSerialAnnotation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MoveVariableDeclarationIntoWhen" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MultipleVariablesInDeclaration" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="NegativeIntConstantInLongContext" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NestedLambdaShadowedImplicitParameter" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="NewClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NewObjectEquality" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NoButtonGroup" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NoScrollPane" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NonAtomicOperationOnVolatileField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NullChecksToSafeCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="NumberEquality" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ObjectEqualsCanBeEquality" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ObjectPrivatePropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ObjectsEqualsCanBeSimplified" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="OctalLiteral" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OptionalAssignedToNull" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OptionalExpectation" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="OptionalGetWithoutIsPresent" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OptionalUsedAsFieldOrParameterType" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PatternVariableHidesField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PlatformExtensionReceiverOfInline" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PointlessBitwiseExpression" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PresentationAnnotation" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="PrimitiveArrayArgumentToVariableArgMethod" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ProtectedInFinal" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PsiElementConcatenation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PublicField" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="RedundantCompareToJavaTime" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantEmptyInitializerBlock" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantExplicitClose" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantFileCreation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantGetter" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantLambdaOrAnonymousFunction" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantLengthCheck" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantNullableReturnType" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantObjectTypeCheck" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="RedundantRecordConstructor" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantScheduledForRemovalAnnotation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantSemicolon" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantSuppression" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantSuspendModifier" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantThrows" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantUnitReturnType" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantUnmodifiable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpDuplicateAlternationBranch" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpEscapedMetaCharacter" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="RegExpRedundantEscape" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpSimplifiable" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpSingleCharAlternation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpUnnecessaryNonCapturingGroup" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RemoveEmptyClassBody" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RemoveEmptyPrimaryConstructor" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RemoveToStringInStringTemplate" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceArrayOfWithLiteral" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceAssociateFunction" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceGuardClauseWithFunctionCall" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ReplaceJavaStaticMethodWithKotlinAnalog" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceRangeStartEndInclusiveWithFirstLast" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceRangeToWithRangeUntil" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceReadLineWithReadln" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceSizeZeroCheckWithIsEmpty" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceSubstringWithDropLast" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="ReplaceWithEnumMap" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ReplaceWithOperatorAssignment" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RequiredAttributes" enabled="false" level="WARNING" enabled_by_default="false">
<option name="myAdditionalRequiredHtmlAttributes" value="" />
</inspection_tool>
<inspection_tool class="ReturnFromFinallyBlock" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SafeCastWithReturn" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ScopeFunctionConversion" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="SetterBackingFieldAssignment" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ShellCheck" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="ShiftOutOfRange" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SimplifiableCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SimplifyBooleanWithConstants" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SimplifyCollector" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SimplifyWhenWithBooleanConstantCondition" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SingleElementAnnotation" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="SingleStatementInBlock" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="SortedCollectionWithNonComparableKeys" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SourceToSinkFlow" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StatefulEp" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StringBufferReplaceableByString" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StringBufferReplaceableByStringBuilder" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StringEquality" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StringEqualsCharSequence" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspendFunctionOnCoroutineScope" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousArrayMethodCall" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousAsDynamic" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousCollectionReassignment" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousEqualsCombination" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousIndentAfterControlStatement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousListRemoveInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousMethodCalls" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousSystemArraycopy" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousTernaryOperatorInVarargsCall" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SynchronizeOnNonFinalField" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SystemRunFinalizersOnExit" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TailRecursion" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="TestFailedLine" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TestFunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ThreadRun" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ThreadWithDefaultRunMethod" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ThrowFromFinallyBlock" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TrailingWhitespacesInTextBlock" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TryStatementWithMultipleResources" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="TypeCustomizer" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnaryPlus" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnknownLanguage" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="UnlabeledReturnInsideLambda" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="UnnecessaryBoxing" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryCallToStringValueOf" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryContinue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryLabelOnContinueStatement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryOptInAnnotation" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryUnaryMinus" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryUnboxing" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessaryUnicodeEscape" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedMessageFormatParameter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UseBulkOperation" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UseCompareMethod" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UseDPIAwareBorders" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UseDPIAwareInsets" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UsePluginIdEquals" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UsePrimitiveTypes" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UselessCallOnCollection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="VariableTypeCanBeExplicit" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="VerboseNullabilityAndEmptiness" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="WaitWhileHoldingTwoLocks" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="WhileCanBeForeach" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="WrongPackageStatement" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="XmlDefaultAttributeValue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="XmlDeprecatedElement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="XmlDuplicatedId" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="XmlUnusedNamespaceDeclaration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="XmlWrongRootElement" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="StructuralWrap" enabled="false" level="TYPO" enabled_by_default="false" />
<inspection_tool class="XsltDeclarations" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="XsltUnusedDeclaration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="XsltVariableShadowing" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="YAMLDuplicatedKeys" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="YAMLRecursiveAlias" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="YAMLSchemaValidation" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@@ -1,5 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="IDE sample" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/samples/ide-plugin/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/ide-plugin" />
@@ -18,6 +19,7 @@
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@@ -1,14 +1,18 @@
[![JetBrains incubator](https://camo.githubusercontent.com/be6f8b50b2400e8b0dc74e58dd9a68803fe6698f5f30d843a7504888879f8392/68747470733a2f2f6a622e67672f6261646765732f696e63756261746f722d706c61737469632e737667)](https://github.com/JetBrains#jetbrains-on-github) [![CI checks](https://github.com/JetBrains/jewel/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/JetBrains/jewel/actions/workflows/build.yml)
# Jewel: a Compose for Desktop theme
<img alt="Jewel logo" src="art/jewel-logo.svg" width="20%"/>
Jewel aims at recreating the _Darcula_ and _New UI_ Swing Look and Feels used on the IntelliJ Platform into Compose for Desktop.
Jewel aims at recreating the _Darcula_ and _New UI_ Swing Look and Feels used on the IntelliJ Platform into Compose for
Desktop.
> **Warning**
>This project is in very early development and is probably not ready to be used in production projects. You _can_, but there
>are no published snapshots, and you should expect APIs to break fairly often, things to move around, and all that jazz.
>Use at your risk!
> This project is in very early development and is probably not ready to be used in production projects. You _can_, but
> there
> are no published snapshots, and you should expect APIs to break fairly often, things to move around, and all that
> jazz.
> Use at your risk!
## Project structure
@@ -19,7 +23,8 @@ The project is split in modules:
3. `themes` are the two themes implemented by Jewel:
1. `darcula` is the old school Intellij LaF, called Darcula, which has two implementations:
1. `darcula-standalone` is the base theme and can be used in any Compose for Desktop project
2. `darcula-ide` 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
2. `darcula-ide` 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. `new-ui` implements the new IntelliJ LaF, known as "new UI". This also has the same two implementations
@@ -31,29 +36,45 @@ The project is split in modules:
To run the stand-alone sample app, you can run the `:samples:standalone:run` Gradle task.
To run the IntelliJ IDEA plugin sample, you can run the `:samples:ide-plugin:runIde` Gradle task. This will download and run a copy of IJ Community
with the plugin installed; you can check the additional panels in the IDE once it starts up (at the bottom, by default, in old UI; in the overflow
To run the IntelliJ IDEA plugin sample, you can run the `:samples:ide-plugin:runIde` Gradle task. This will download and
run a copy of IJ Community
with the plugin installed; you can check the additional panels in the IDE once it starts up (at the bottom, by default,
in old UI; in the overflow
in the new UI).
If you're using IntelliJ IDEA, you can use the "Stand-alone sample" and "IDE sample" run configurations.
### The Swing Bridge
In the `*-ide` modules, there is a crucial element for proper integration with the IDE: a bridge between the Swing theme and LaF, and the Compose
In the `*-ide` modules, 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
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
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
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
[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.
To see debug logs in the IDE, add these to __Help | Diagnostic Tools | Debug Log Settings__:
```
#org.jetbrains.jewel.demo
#org.jetbrains.jewel
```

View File

@@ -10,7 +10,6 @@ val sarif: Configuration by configurations.creating {
}
dependencies {
sarif(projects.foundation)
sarif(projects.core)
sarif(projects.composeUtils)
sarif(projects.samples.standalone)

View File

@@ -35,6 +35,7 @@ kotlin {
optIn("kotlin.experimental.ExperimentalTypeInference")
optIn("androidx.compose.ui.ExperimentalComposeUiApi")
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
optIn("org.jetbrains.jewel.ExperimentalJewelApi")
}
}
}

View File

@@ -2,7 +2,6 @@ package org.jetbrains.jewel.buildlogic.theme
import com.squareup.kotlinpoet.ClassName
import io.gitlab.arturbosch.detekt.Detekt
import java.net.URL
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@@ -23,7 +22,8 @@ import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import org.gradle.util.internal.GUtil
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
import java.net.URL
abstract class IntelliJThemeGeneratorPlugin : Plugin<Project> {
@@ -42,7 +42,7 @@ abstract class IntelliJThemeGeneratorPlugin : Plugin<Project> {
ideaVersion.set(this@all.ideaVersion)
themeFile.set(this@all.themeFile)
}
tasks.withType<KotlinCompile> {
tasks.withType<BaseKotlinCompile> {
dependsOn(task)
}
tasks.withType<Detekt> {

View File

@@ -7,6 +7,5 @@ dependencies {
api(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
implementation(libs.jna)
implementation(libs.kotlinx.serialization.json)
}

View File

@@ -6,6 +6,5 @@ plugins {
dependencies {
api(projects.composeUtils)
api(projects.foundation)
api(compose.desktop.common)
}

View File

@@ -29,11 +29,9 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import org.jetbrains.jewel.CommonStateBitMask.Active
import org.jetbrains.jewel.CommonStateBitMask.Enabled
import org.jetbrains.jewel.CommonStateBitMask.Error
import org.jetbrains.jewel.CommonStateBitMask.Focused
import org.jetbrains.jewel.CommonStateBitMask.Hovered
import org.jetbrains.jewel.CommonStateBitMask.Pressed
import org.jetbrains.jewel.CommonStateBitMask.Warning
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.border
import org.jetbrains.jewel.styling.ButtonStyle
@@ -55,7 +53,7 @@ fun DefaultButton(
interactionSource = interactionSource,
style = style,
content = content,
textStyle = textStyle
textStyle = textStyle,
)
}
@@ -76,7 +74,7 @@ fun OutlinedButton(
interactionSource = interactionSource,
style = style,
content = content,
textStyle = textStyle
textStyle = textStyle,
)
}
@@ -117,23 +115,27 @@ private fun ButtonImpl(
val shape = RoundedCornerShape(style.metrics.cornerSize)
val colors = style.colors
val borderColor by colors.borderFor(buttonState)
println("state: $buttonState ($enabled) -> $borderColor")
Box(
modifier.clickable(
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
).background(colors.backgroundFor(buttonState).value, shape)
modifier = modifier
.clickable(
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null,
)
.background(colors.backgroundFor(buttonState).value, shape)
.border(Stroke.Alignment.Center, style.metrics.borderWidth, borderColor, shape)
.focusOutline(buttonState, shape),
propagateMinConstraints = true
propagateMinConstraints = true,
) {
val contentColor by colors.contentFor(buttonState)
CompositionLocalProvider(
LocalContentColor provides contentColor.takeOrElse { textStyle.color },
LocalTextStyle provides textStyle.copy(color = contentColor.takeOrElse { textStyle.color })
LocalTextStyle provides textStyle.copy(color = contentColor.takeOrElse { textStyle.color }),
) {
Row(
Modifier
@@ -141,7 +143,7 @@ private fun ButtonImpl(
.padding(style.metrics.padding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content
content = content,
)
}
}
@@ -182,7 +184,7 @@ value class ButtonState(val state: ULong) : FocusableComponentState {
focused = focused,
pressed = pressed,
hovered = hovered,
active = active
active = active,
)
override fun toString() =
@@ -194,19 +196,15 @@ value class ButtonState(val state: ULong) : FocusableComponentState {
fun of(
enabled: Boolean = true,
focused: Boolean = false,
error: Boolean = false,
pressed: Boolean = false,
hovered: Boolean = false,
warning: Boolean = false,
active: Boolean = true,
) = ButtonState(
state = (if (enabled) Enabled else 0UL) or
(if (focused) Focused else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (warning) Warning else 0UL) or
(if (error) Error else 0UL) or
(if (active) Active else 0UL)
(if (active) Active else 0UL),
)
}
}

View File

@@ -71,7 +71,7 @@ fun Checkbox(
icons = icons,
textStyle = textStyle,
resourceLoader = resourceLoader,
content = null
content = null,
)
}
@@ -101,7 +101,7 @@ fun TriStateCheckbox(
icons = icons,
textStyle = textStyle,
resourceLoader = resourceLoader,
content = null
content = null,
)
}
@@ -131,7 +131,7 @@ fun TriStateCheckboxRow(
metrics = metrics,
icons = icons,
resourceLoader = resourceLoader,
textStyle = textStyle
textStyle = textStyle,
) {
Text(text)
}
@@ -165,7 +165,7 @@ fun CheckboxRow(
metrics = metrics,
icons = icons,
resourceLoader = resourceLoader,
textStyle = textStyle
textStyle = textStyle,
) {
Text(text)
}
@@ -200,7 +200,7 @@ fun CheckboxRow(
icons = icons,
resourceLoader = resourceLoader,
textStyle = textStyle,
content = content
content = content,
)
}
@@ -231,7 +231,7 @@ fun TriStateCheckboxRow(
icons = icons,
resourceLoader = resourceLoader,
textStyle = textStyle,
content = content
content = content,
)
}
@@ -280,16 +280,16 @@ private fun CheckboxImpl(
enabled = enabled,
role = Role.Checkbox,
interactionSource = interactionSource,
indication = null
indication = null,
)
val checkBoxImageModifier = Modifier.size(metrics.checkboxSize)
.outline(
state = checkboxState,
outline = outline,
alignment = Stroke.Alignment.Center,
outlineShape = RoundedCornerShape(metrics.checkboxCornerSize),
outlineWidth = metrics.outlineWidth
alignment = Stroke.Alignment.Center,
outlineWidth = metrics.outlineWidth,
)
val checkboxPainter by icons.checkbox.getPainter(checkboxState, resourceLoader)
@@ -300,14 +300,14 @@ private fun CheckboxImpl(
Row(
wrapperModifier,
horizontalArrangement = Arrangement.spacedBy(metrics.iconContentGap),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
CheckBoxImage(Modifier, checkboxPainter, checkBoxImageModifier)
val contentColor by colors.contentFor(checkboxState)
CompositionLocalProvider(
LocalTextStyle provides textStyle.copy(color = contentColor.takeOrElse { textStyle.color }),
LocalContentColor provides contentColor.takeOrElse { textStyle.color }
LocalContentColor provides contentColor.takeOrElse { textStyle.color },
) {
content()
}
@@ -367,7 +367,7 @@ value class CheckboxState(private val state: ULong) : ToggleableComponentState {
focused = focused,
pressed = pressed,
hovered = hovered,
active = active
active = active,
)
override fun toString() =
@@ -390,7 +390,7 @@ value class CheckboxState(private val state: ULong) : ToggleableComponentState {
(if (pressed) Pressed else 0UL) or
(if (toggleableState != ToggleableState.Off) Selected else 0UL) or
(if (toggleableState == ToggleableState.Indeterminate) Indeterminate else 0UL) or
(if (active) Active else 0UL)
(if (active) Active else 0UL),
)
}
}

View File

@@ -8,8 +8,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -26,11 +27,10 @@ import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.semantics.Role
import org.jetbrains.jewel.CommonStateBitMask.Active
import org.jetbrains.jewel.CommonStateBitMask.Enabled
import org.jetbrains.jewel.CommonStateBitMask.Error
import org.jetbrains.jewel.CommonStateBitMask.Focused
import org.jetbrains.jewel.CommonStateBitMask.Hovered
import org.jetbrains.jewel.CommonStateBitMask.Pressed
import org.jetbrains.jewel.CommonStateBitMask.Warning
import org.jetbrains.jewel.CommonStateBitMask.Selected
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.border
import org.jetbrains.jewel.styling.ChipStyle
@@ -40,15 +40,95 @@ fun Chip(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
enabled: Boolean = true,
selected: Boolean = false,
style: ChipStyle = IntelliJTheme.chipStyle,
onChipClick: () -> Unit = {},
onClick: () -> Unit = {},
content: @Composable () -> Unit,
) {
ChipImpl(
interactionSource = interactionSource,
enabled = enabled,
selected = selected,
style = style,
modifier = modifier.clickable(
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null,
),
content = content,
)
}
@Composable
fun ToggleableChip(
checked: Boolean,
onClick: (Boolean) -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
enabled: Boolean = true,
style: ChipStyle = IntelliJTheme.chipStyle,
content: @Composable () -> Unit,
) {
ChipImpl(
interactionSource = interactionSource,
enabled = enabled,
selected = checked,
style = style,
modifier = modifier.toggleable(
onValueChange = onClick,
enabled = enabled,
role = Role.Checkbox,
interactionSource = interactionSource,
indication = null,
value = checked,
),
content = content,
)
}
@Composable
fun RadioButtonChip(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
enabled: Boolean = true,
style: ChipStyle = IntelliJTheme.chipStyle,
content: @Composable () -> Unit,
) {
ChipImpl(
interactionSource,
enabled,
selected,
style,
modifier.selectable(
onClick = onClick,
enabled = enabled,
role = Role.RadioButton,
interactionSource = interactionSource,
indication = null,
selected = selected,
),
content,
)
}
@Composable
private fun ChipImpl(
interactionSource: MutableInteractionSource,
enabled: Boolean,
selected: Boolean,
style: ChipStyle,
modifier: Modifier,
content: @Composable () -> Unit,
) {
var chipState by remember(interactionSource) {
mutableStateOf(ChipState.of(enabled = enabled))
mutableStateOf(ChipState.of(enabled = enabled, selected = selected))
}
remember(enabled) {
chipState = chipState.copy(enabled = enabled)
remember(enabled, selected) {
chipState = chipState.copy(enabled = enabled, selected = selected)
}
LaunchedEffect(interactionSource) {
@@ -70,20 +150,12 @@ fun Chip(
Row(
modifier = modifier
.clickable(
onClick = onChipClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
)
.padding(style.metrics.padding)
.defaultMinSize(style.metrics.minSize.width, style.metrics.minSize.height)
.background(colors.backgroundFor(chipState).value, shape)
.border(Stroke.Alignment.Center, style.metrics.borderWidth, borderColor, shape)
.outline(chipState, shape),
.focusOutline(chipState, shape)
.padding(style.metrics.padding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
horizontalArrangement = Arrangement.Center,
) {
CompositionLocalProvider(
LocalContentColor provides (
@@ -92,7 +164,7 @@ fun Chip(
.value
.takeIf { !it.isUnspecified }
?: LocalContentColor.current
)
),
) {
content()
}
@@ -101,7 +173,7 @@ fun Chip(
@Immutable
@JvmInline
value class ChipState(val state: ULong) : StateWithOutline {
value class ChipState(val state: ULong) : FocusableComponentState, SelectableComponentState {
@Stable
override val isActive: Boolean
@@ -116,12 +188,8 @@ value class ChipState(val state: ULong) : StateWithOutline {
get() = state and Focused != 0UL
@Stable
override val isError: Boolean
get() = state and Error != 0UL
@Stable
override val isWarning: Boolean
get() = state and Warning != 0UL
override val isSelected: Boolean
get() = state and Selected != 0UL
@Stable
override val isHovered: Boolean
@@ -134,23 +202,21 @@ value class ChipState(val state: ULong) : StateWithOutline {
fun copy(
enabled: Boolean = isEnabled,
focused: Boolean = isFocused,
error: Boolean = isError,
selected: Boolean = isSelected,
pressed: Boolean = isPressed,
hovered: Boolean = isHovered,
warning: Boolean = isWarning,
active: Boolean = isActive,
): ChipState = of(
enabled = enabled,
focused = focused,
error = error,
pressed = pressed,
hovered = hovered,
warning = warning,
active = active
active = active,
selected = selected,
)
override fun toString() =
"ChipState(isEnabled=$isEnabled, isFocused=$isFocused, isError=$isError, isWarning=$isWarning, " +
"ChipState(isEnabled=$isEnabled, isFocused=$isFocused, isSelected=$isSelected, " +
"isHovered=$isHovered, isPressed=$isPressed, isActive=$isActive)"
companion object {
@@ -158,19 +224,17 @@ value class ChipState(val state: ULong) : StateWithOutline {
fun of(
enabled: Boolean = true,
focused: Boolean = false,
error: Boolean = false,
selected: Boolean = false,
pressed: Boolean = false,
hovered: Boolean = false,
warning: Boolean = false,
active: Boolean = false,
) = ChipState(
state = (if (enabled) Enabled else 0UL) or
(if (focused) Focused else 0UL) or
(if (error) Error else 0UL) or
(if (selected) Selected else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (warning) Warning else 0UL) or
(if (active) Active else 0UL)
(if (active) Active else 0UL),
)
}
}

View File

@@ -15,7 +15,9 @@ import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.res.ResourceLoader
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.rememberCursorPositionProvider
import org.jetbrains.jewel.styling.MenuStyle
@@ -31,7 +33,8 @@ object IntelliJContextMenuRepresentation : ContextMenuRepresentation {
state.status = ContextMenuState.Status.Closed
true
},
style = IntelliJTheme.menuStyle
style = IntelliJTheme.menuStyle,
resourceLoader = LocalResourceLoader.current,
) {
contextItems(items)
}
@@ -42,8 +45,9 @@ object IntelliJContextMenuRepresentation : ContextMenuRepresentation {
@Composable
internal fun ContextMenu(
onDismissRequest: (InputMode) -> Boolean,
focusable: Boolean = true,
resourceLoader: ResourceLoader,
modifier: Modifier = Modifier,
focusable: Boolean = true,
style: MenuStyle = IntelliJTheme.menuStyle,
content: MenuScope.() -> Unit,
) {
@@ -51,31 +55,33 @@ internal fun ContextMenu(
var inputModeManager: InputModeManager? by mutableStateOf(null)
val menuManager = remember(onDismissRequest) {
MenuManager(
onDismissRequest = onDismissRequest
onDismissRequest = onDismissRequest,
)
}
Popup(
focusable = focusable,
popupPositionProvider = rememberCursorPositionProvider(style.metrics.offset),
onDismissRequest = {
onDismissRequest(InputMode.Touch)
},
popupPositionProvider = rememberCursorPositionProvider(style.metrics.offset),
properties = PopupProperties(focusable = focusable),
onPreviewKeyEvent = { false },
onKeyEvent = {
val currentFocusManager = checkNotNull(focusManager) { "FocusManager must not be null" }
val currentInputModeManager = checkNotNull(inputModeManager) { "InputModeManager must not be null" }
handlePopupMenuOnKeyEvent(it, currentFocusManager, currentInputModeManager, menuManager)
}
},
) {
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
CompositionLocalProvider(
LocalMenuManager provides menuManager
LocalMenuManager provides menuManager,
) {
MenuContent(
modifier = modifier,
content = content
content = content,
resourceLoader = resourceLoader,
)
}
}
@@ -85,7 +91,7 @@ private fun MenuScope.contextItems(items: () -> List<ContextMenuItem>) {
items().forEach { item ->
when (item) {
is ContextMenuDivider -> {
divider()
separator()
}
is ContextSubmenu -> {
@@ -99,7 +105,7 @@ private fun MenuScope.contextItems(items: () -> List<ContextMenuItem>) {
else -> {
selectableItem(
selected = false,
onClick = item.onClick
onClick = item.onClick,
) {
Text(item.label)
}

View File

@@ -0,0 +1,29 @@
package org.jetbrains.jewel
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
// Implements javax.swing.GrayFilter's behaviour with percent = 50, brighter = true
// to match the GrayFilter#createDisabledImage behavior, used by Swing.
private val disabledColorMatrixGammaEncoded = ColorMatrix().apply {
val saturation = .5f
// We use NTSC luminance weights like Swing does as it's gamma-encoded RGB
val redFactor = .299f * saturation
val greenFactor = .587f * saturation
val blueFactor = .114f * saturation
// TODO we should also be scaling the brightness but it's not possible
// with a matrix transformation as far as I can tell
this[0, 0] = redFactor
this[0, 1] = greenFactor
this[0, 2] = blueFactor
this[1, 0] = redFactor
this[1, 1] = greenFactor
this[1, 2] = blueFactor
this[2, 0] = redFactor
this[2, 1] = greenFactor
this[2, 2] = blueFactor
}
fun ColorFilter.Companion.disabled(): ColorFilter = colorMatrix(disabledColorMatrixGammaEncoded)

View File

@@ -36,6 +36,6 @@ fun Divider(
modifier
.then(indentMod)
.then(orientationModifier)
.background(color = color)
.background(color = color),
)
}

View File

@@ -34,13 +34,12 @@ import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.res.ResourceLoader
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import org.jetbrains.jewel.CommonStateBitMask.Active
import org.jetbrains.jewel.CommonStateBitMask.Enabled
import org.jetbrains.jewel.CommonStateBitMask.Error
import org.jetbrains.jewel.CommonStateBitMask.Focused
import org.jetbrains.jewel.CommonStateBitMask.Hovered
import org.jetbrains.jewel.CommonStateBitMask.Pressed
import org.jetbrains.jewel.CommonStateBitMask.Warning
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.border
import org.jetbrains.jewel.styling.DropdownStyle
@@ -64,11 +63,11 @@ fun Dropdown(
var skipNextClick by remember { mutableStateOf(false) }
var dropdownState by remember(interactionSource) {
mutableStateOf(DropdownState.of(enabled = enabled, outline = outline))
mutableStateOf(DropdownState.of(enabled = enabled))
}
remember(enabled, outline) {
dropdownState = dropdownState.copy(enabled = enabled, outline = Outline.Error)
remember(enabled) {
dropdownState = dropdownState.copy(enabled = enabled)
}
LaunchedEffect(interactionSource) {
@@ -91,6 +90,7 @@ fun Dropdown(
val metrics = style.metrics
val shape = RoundedCornerShape(style.metrics.cornerSize)
val minSize = metrics.minSize
val arrowMinSize = style.metrics.arrowMinSize
val borderColor by colors.borderFor(dropdownState)
Box(
@@ -105,33 +105,37 @@ fun Dropdown(
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
).background(colors.backgroundFor(dropdownState).value, shape)
indication = null,
)
.background(colors.backgroundFor(dropdownState).value, shape)
.border(Stroke.Alignment.Center, style.metrics.borderWidth, borderColor, shape)
.defaultMinSize(minSize.width, minSize.height),
contentAlignment = Alignment.CenterStart
.outline(dropdownState, outline, shape)
.defaultMinSize(minSize.width, minSize.height.coerceAtLeast(arrowMinSize.height)),
contentAlignment = Alignment.CenterStart,
) {
CompositionLocalProvider(
LocalContentColor provides colors.contentFor(dropdownState).value
LocalContentColor provides colors.contentFor(dropdownState).value,
) {
Row(
Modifier.padding(style.metrics.contentPadding).padding(end = minSize.height),
modifier = Modifier.padding(style.metrics.contentPadding)
.padding(end = minSize.height),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
content = {
content()
}
},
)
Box(
modifier = Modifier.size(minSize.height).align(Alignment.CenterEnd),
contentAlignment = Alignment.Center
modifier = Modifier.size(arrowMinSize)
.align(Alignment.CenterEnd),
contentAlignment = Alignment.Center,
) {
val chevronIcon by style.icons.chevronDown.getPainter(dropdownState, resourceLoader)
Icon(
painter = chevronIcon,
contentDescription = null,
tint = colors.iconTintFor(dropdownState).value
tint = colors.iconTintFor(dropdownState).value,
)
}
}
@@ -149,7 +153,8 @@ fun Dropdown(
modifier = menuModifier,
style = style.menuStyle,
horizontalAlignment = Alignment.Start,
content = menuContent
content = menuContent,
resourceLoader = resourceLoader,
)
}
}
@@ -159,6 +164,7 @@ fun Dropdown(
internal fun DropdownMenu(
onDismissRequest: (InputMode) -> Boolean,
horizontalAlignment: Alignment.Horizontal,
resourceLoader: ResourceLoader,
modifier: Modifier = Modifier,
style: MenuStyle,
content: MenuScope.() -> Unit,
@@ -167,9 +173,9 @@ internal fun DropdownMenu(
val popupPositionProvider = AnchorVerticalMenuPositionProvider(
contentOffset = style.metrics.offset,
contentMargin = style.metrics.margin,
contentMargin = style.metrics.menuMargin,
alignment = horizontalAlignment,
density = density
density = density,
)
var focusManager: FocusManager? by mutableStateOf(null)
@@ -179,25 +185,27 @@ internal fun DropdownMenu(
}
Popup(
onDismissRequest = { onDismissRequest(InputMode.Touch) },
popupPositionProvider = popupPositionProvider,
onDismissRequest = { onDismissRequest(InputMode.Touch) },
properties = PopupProperties(focusable = true),
onPreviewKeyEvent = { false },
onKeyEvent = {
val currentFocusManager = checkNotNull(focusManager) { "FocusManager must not be null" }
val currentInputModeManager = checkNotNull(inputModeManager) { "InputModeManager must not be null" }
handlePopupMenuOnKeyEvent(it, currentFocusManager, currentInputModeManager, menuManager)
},
focusable = true
) {
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
CompositionLocalProvider(
LocalMenuManager provides menuManager,
LocalMenuStyle provides style
LocalMenuStyle provides style,
) {
MenuContent(
modifier = modifier,
content = content
content = content,
resourceLoader = resourceLoader,
)
}
}
@@ -205,7 +213,7 @@ internal fun DropdownMenu(
@Immutable
@JvmInline
value class DropdownState(val state: ULong) : StateWithOutline {
value class DropdownState(val state: ULong) : FocusableComponentState {
@Stable
override val isActive: Boolean
@@ -219,14 +227,6 @@ value class DropdownState(val state: ULong) : StateWithOutline {
override val isFocused: Boolean
get() = state and Focused != 0UL
@Stable
override val isError: Boolean
get() = state and Error != 0UL
@Stable
override val isWarning: Boolean
get() = state and Warning != 0UL
@Stable
override val isHovered: Boolean
get() = state and Hovered != 0UL
@@ -240,19 +240,17 @@ value class DropdownState(val state: ULong) : StateWithOutline {
focused: Boolean = isFocused,
pressed: Boolean = isPressed,
hovered: Boolean = isHovered,
outline: Outline = Outline.of(isWarning, isError),
active: Boolean = isActive,
) = of(
enabled = enabled,
focused = focused,
pressed = pressed,
hovered = hovered,
outline = outline,
active = active
active = active,
)
override fun toString() =
"${javaClass.simpleName}(isEnabled=$isEnabled, isFocused=$isFocused, isError=$isError, isWarning=$isWarning, " +
"${javaClass.simpleName}(isEnabled=$isEnabled, isFocused=$isFocused, " +
"isHovered=$isHovered, isPressed=$isPressed, isActive=$isActive)"
companion object {
@@ -262,7 +260,6 @@ value class DropdownState(val state: ULong) : StateWithOutline {
focused: Boolean = false,
pressed: Boolean = false,
hovered: Boolean = false,
outline: Outline = Outline.None,
active: Boolean = false,
) = DropdownState(
if (enabled) {
@@ -272,10 +269,8 @@ value class DropdownState(val state: ULong) : StateWithOutline {
(if (focused) Focused else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (outline == Outline.Error) Error else 0UL) or
(if (outline == Outline.Warning) Warning else 0UL) or
(if (active) Active else 0UL)
}
},
)
}
}

View File

@@ -3,10 +3,10 @@ package org.jetbrains.jewel
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "This is an experimental API for Jewel and is likely to change before becoming " +
"stable."
"stable.",
)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION
AnnotationTarget.FUNCTION,
)
annotation class ExperimentalJewelApi

View File

@@ -8,41 +8,33 @@ import androidx.compose.ui.graphics.Color
interface GlobalColors {
val borders: BorderColors
val outlines: OutlineColors
@SwingLafKey("*.infoForeground")
val infoContent: Color
}
@Immutable
interface BorderColors {
@SwingLafKey("Component.borderColor")
val normal: Color
@SwingLafKey("Component.focusedBorderColor")
val focused: Color
@SwingLafKey("*.disabledBorderColor")
val disabled: Color
}
@Immutable
interface OutlineColors {
@SwingLafKey("*.focusColor")
val focused: Color
@SwingLafKey("Component.warningFocusColor")
val focusedWarning: Color
@SwingLafKey("Component.errorFocusColor")
val focusedError: Color
@SwingLafKey("Component.inactiveWarningFocusColor")
val warning: Color
@SwingLafKey("Component.inactiveErrorFocusColor")
val error: Color
}

View File

@@ -1,6 +1,5 @@
package org.jetbrains.jewel
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
@@ -9,7 +8,6 @@ import androidx.compose.ui.unit.Dp
interface GlobalMetrics {
val outlineWidth: Dp
val outlineCornerSize: CornerSize
}
val LocalGlobalMetrics = staticCompositionLocalOf<GlobalMetrics> {

View File

@@ -15,7 +15,7 @@ fun GroupHeader(
style: GroupHeaderStyle = LocalGroupHeaderStyle.current,
) {
CompositionLocalProvider(
LocalContentColor provides style.colors.content
LocalContentColor provides style.colors.content,
) {
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
Text(text)
@@ -23,7 +23,7 @@ fun GroupHeader(
color = style.colors.divider,
orientation = Orientation.Horizontal,
startIndent = style.metrics.indent,
thickness = style.metrics.dividerThickness
thickness = style.metrics.dividerThickness,
)
}
}

View File

@@ -7,13 +7,13 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toolingGraphicsLayer
@@ -32,7 +32,6 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.xml.sax.InputSource
import java.io.InputStream
import java.util.jar.JarFile
/**
* Icon component that draws [imageVector] using [tint], defaulting to
@@ -58,7 +57,7 @@ fun Icon(
painter = rememberVectorPainter(imageVector),
contentDescription = contentDescription,
modifier = modifier,
tint = tint
tint = tint,
)
}
@@ -87,7 +86,7 @@ fun Icon(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
tint = tint
tint = tint,
)
}
@@ -95,15 +94,15 @@ fun Icon(
fun Icon(
resource: String,
contentDescription: String?,
resourceLoader: ResourceLoader = LocalResourceLoader.current,
modifier: Modifier = Modifier,
resourceLoader: ResourceLoader = LocalResourceLoader.current,
tint: Color = Color.Unspecified,
) {
Icon(
painter = painterResource(resource, resourceLoader),
contentDescription = contentDescription,
modifier = modifier,
tint = tint
tint = tint,
)
}
@@ -123,11 +122,32 @@ fun Icon(
@Composable
fun Icon(
painter: Painter,
contentDescription: String? = null,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
) {
val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
val colorFilter = if (tint.isSpecified) ColorFilter.tint(tint) else null
Icon(painter, contentDescription, colorFilter, modifier)
}
/**
* Icon component that draws a [painter] using a [colorFilter]
*
* @param painter [Painter] to draw inside this Icon
* @param contentDescription text used by accessibility services to
* describe what this icon represents. This should always be
* provided unless this icon is used for decorative purposes, and
* does not represent a meaningful action that a user can take.
* @param modifier optional [Modifier] for this Icon
* @param colorFilter color filter to be applied to [painter]
*/
@Composable
fun Icon(
painter: Painter,
contentDescription: String?,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
) {
val semantics = if (contentDescription != null) {
Modifier.semantics {
this.contentDescription = contentDescription
@@ -142,9 +162,9 @@ fun Icon(
.paint(
painter,
colorFilter = colorFilter,
contentScale = ContentScale.Fit
contentScale = ContentScale.Fit,
)
.then(semantics)
.then(semantics),
)
}
@@ -202,47 +222,16 @@ private inline fun <T> useResource(
block: (InputStream) -> T,
): T = loader.load(resourcePath).use(block)
val LocalResourceLoader = staticCompositionLocalOf<ResourceLoader> {
ResourceLoader.Default
}
private fun Modifier.defaultSizeFor(painter: Painter) =
this.then(
then(
if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) {
DefaultIconSizeModifier
} else {
Modifier
}
},
)
private fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
// Default icon size, for icons with no intrinsic size information
private val DefaultIconSizeModifier = Modifier.size(16.dp)
fun getJarPath(klass: Class<*>): String? {
val className = klass.name.replace('.', '/') + ".class"
val classPath = klass.classLoader.getResource(className)?.toString() ?: return null
if (!classPath.startsWith("jar")) {
// Class not from a JAR
return null
}
return classPath.substringBefore("!").removePrefix("jar:file:")
}
fun extractFileFromJar(jarPath: String, filePath: String) =
JarFile(jarPath).use { jar ->
jar.getEntry(filePath)?.let { entry ->
jar.getInputStream(entry).use { it.readBytes().inputStream() }
}
}
inline fun <reified T> getJarPath(): String? = getJarPath(T::class.java)
class RawJarResourceLoader(private val jars: List<String>) : ResourceLoader {
override fun load(resourcePath: String): InputStream =
jars.mapNotNull { jarPath ->
extractFileFromJar(jarPath, resourcePath)
}.firstOrNull() ?: error("Resource $resourcePath not found in jars $jars")
}

View File

@@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
@@ -33,7 +34,7 @@ internal fun InputField(
modifier: Modifier,
enabled: Boolean,
readOnly: Boolean,
isError: Boolean,
outline: Outline,
undecorated: Boolean,
visualTransformation: VisualTransformation,
keyboardOptions: KeyboardOptions,
@@ -47,16 +48,15 @@ internal fun InputField(
decorationBox: @Composable (innerTextField: @Composable () -> Unit, state: InputFieldState) -> Unit,
) {
var inputState by remember(interactionSource) {
mutableStateOf(InputFieldState.of(enabled = enabled, error = isError))
mutableStateOf(InputFieldState.of(enabled = enabled))
}
remember(isError, enabled) {
inputState = inputState.copy(error = isError, enabled = enabled)
remember(enabled) {
inputState = inputState.copy(enabled = enabled)
}
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> inputState = inputState.copy(focused = true)
is FocusInteraction.Unfocus -> inputState = inputState.copy(focused = false)
}
}
@@ -73,26 +73,28 @@ internal fun InputField(
val borderColor by style.colors.borderFor(inputState)
val borderModifier = Modifier.appendIf(!undecorated && borderColor.isSpecified) {
Modifier.border(
alignment = Stroke.Alignment.Outside,
alignment = Stroke.Alignment.Center,
width = style.metrics.borderWidth,
color = borderColor,
shape = shape
shape = shape,
)
}
val contentColor by colors.contentFor(inputState)
val mergedTextStyle = style.textStyle.merge(textStyle).copy(color = contentColor)
val cursorBrush by colors.cursorFor(inputState)
val caretColor by colors.caretFor(inputState)
BasicTextField(
value = value,
modifier = modifier.then(backgroundModifier)
.then(borderModifier),
.then(borderModifier)
.focusOutline(inputState, shape)
.outline(inputState, outline, shape),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = cursorBrush,
cursorBrush = SolidColor(caretColor),
visualTransformation = visualTransformation,
onTextLayout = onTextLayout,
keyboardOptions = keyboardOptions,
@@ -102,13 +104,13 @@ internal fun InputField(
maxLines = maxLines,
decorationBox = @Composable { innerTextField: @Composable () -> Unit ->
decorationBox(innerTextField, inputState)
}
},
)
}
@Immutable
@JvmInline
value class InputFieldState(val state: ULong) : StateWithOutline {
value class InputFieldState(val state: ULong) : FocusableComponentState {
@Stable
override val isActive: Boolean
@@ -122,14 +124,6 @@ value class InputFieldState(val state: ULong) : StateWithOutline {
override val isFocused: Boolean
get() = state and CommonStateBitMask.Focused != 0UL
@Stable
override val isError: Boolean
get() = state and CommonStateBitMask.Error != 0UL
@Stable
override val isWarning: Boolean
get() = state and CommonStateBitMask.Warning != 0UL
@Stable
override val isHovered: Boolean
get() = state and CommonStateBitMask.Hovered != 0UL
@@ -141,23 +135,19 @@ value class InputFieldState(val state: ULong) : StateWithOutline {
fun copy(
enabled: Boolean = isEnabled,
focused: Boolean = isFocused,
error: Boolean = isError,
pressed: Boolean = isPressed,
hovered: Boolean = isHovered,
warning: Boolean = isWarning,
active: Boolean = isActive,
) = of(
enabled = enabled,
focused = focused,
error = error,
pressed = pressed,
hovered = hovered,
warning = warning,
active = active
active = active,
)
override fun toString() =
"${javaClass.simpleName}(isEnabled=$isEnabled, isFocused=$isFocused, isError=$isError, isWarning=$isWarning, " +
"${javaClass.simpleName}(isEnabled=$isEnabled, isFocused=$isFocused, " +
"isHovered=$isHovered, isPressed=$isPressed, isActive=$isActive)"
companion object {
@@ -165,19 +155,15 @@ value class InputFieldState(val state: ULong) : StateWithOutline {
fun of(
enabled: Boolean = true,
focused: Boolean = false,
error: Boolean = false,
pressed: Boolean = false,
hovered: Boolean = false,
warning: Boolean = false,
active: Boolean = false,
) = InputFieldState(
state = (if (enabled) CommonStateBitMask.Enabled else 0UL) or
(if (focused) CommonStateBitMask.Focused else 0UL) or
(if (error) CommonStateBitMask.Error else 0UL) or
(if (hovered) CommonStateBitMask.Hovered else 0UL) or
(if (pressed) CommonStateBitMask.Pressed else 0UL) or
(if (warning) CommonStateBitMask.Warning else 0UL) or
(if (active) CommonStateBitMask.Active else 0UL)
(if (active) CommonStateBitMask.Active else 0UL),
)
}
}

View File

@@ -19,23 +19,23 @@ import org.jetbrains.jewel.styling.TextFieldStyle
@Stable
class IntelliJComponentStyling(
val defaultButtonStyle: ButtonStyle,
val outlinedButtonStyle: ButtonStyle,
val checkboxStyle: CheckboxStyle,
val chipStyle: ChipStyle,
val defaultButtonStyle: ButtonStyle,
val defaultTabStyle: TabStyle,
val dropdownStyle: DropdownStyle,
val editorTabStyle: TabStyle,
val groupHeaderStyle: GroupHeaderStyle,
val horizontalProgressBarStyle: HorizontalProgressBarStyle,
val labelledTextFieldStyle: LabelledTextFieldStyle,
val lazyTreeStyle: LazyTreeStyle,
val linkStyle: LinkStyle,
val menuStyle: MenuStyle,
val horizontalProgressBarStyle: HorizontalProgressBarStyle,
val outlinedButtonStyle: ButtonStyle,
val radioButtonStyle: RadioButtonStyle,
val scrollbarStyle: ScrollbarStyle,
val textAreaStyle: TextAreaStyle,
val textFieldStyle: TextFieldStyle,
val lazyTreeStyle: LazyTreeStyle,
val defaultTabStyle: TabStyle,
val editorTabStyle: TabStyle,
) {
override fun equals(other: Any?): Boolean {
@@ -85,4 +85,13 @@ class IntelliJComponentStyling(
result = 31 * result + editorTabStyle.hashCode()
return result
}
override fun toString(): String =
"IntelliJComponentStyling(checkboxStyle=$checkboxStyle, chipStyle=$chipStyle, " +
"defaultButtonStyle=$defaultButtonStyle, defaultTabStyle=$defaultTabStyle, dropdownStyle=$dropdownStyle, " +
"editorTabStyle=$editorTabStyle, groupHeaderStyle=$groupHeaderStyle, " +
"horizontalProgressBarStyle=$horizontalProgressBarStyle, labelledTextFieldStyle=$labelledTextFieldStyle, " +
"lazyTreeStyle=$lazyTreeStyle, linkStyle=$linkStyle, menuStyle=$menuStyle, " +
"outlinedButtonStyle=$outlinedButtonStyle, radioButtonStyle=$radioButtonStyle, " +
"scrollbarStyle=$scrollbarStyle, textAreaStyle=$textAreaStyle, textFieldStyle=$textFieldStyle)"
}

View File

@@ -1,4 +1,4 @@
package org.jetbrains.jewel.themes.intui.core
package org.jetbrains.jewel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@@ -7,8 +7,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.ResourceLoader
import androidx.compose.ui.res.loadSvgPainter
import org.jetbrains.jewel.SvgLoader
import org.jetbrains.jewel.SvgPatcher
import java.io.InputStream
import java.util.concurrent.ConcurrentHashMap
@@ -39,22 +37,23 @@ class IntelliJSvgLoader(private val svgPatcher: SvgPatcher) : SvgLoader {
): Painter {
val density = LocalDensity.current
val painter = try {
useResource(resourcePath, loader) {
loadSvgPainter(it.patchColors(), density)
val painter =
try {
useResource(resourcePath, loader) {
loadSvgPainter(it.patchColors(), density)
}
} catch (e: IllegalArgumentException) {
val simplerPath = trySimplifyingPath(resourcePath)
if (simplerPath != null) {
System.err.println("Unable to load '$resourcePath' (base: $basePath), trying simpler version: $simplerPath")
return rememberPatchedSvgResource(basePath, simplerPath, loader)
} else {
throw IllegalArgumentException(
"Unable to load '$resourcePath' (base: $basePath), no simpler version available",
e,
)
}
}
} catch (e: IllegalArgumentException) {
val simplerPath = trySimplifyingPath(resourcePath)
if (simplerPath != null) {
System.err.println("Unable to load '$resourcePath' (base: $basePath), trying simpler version: $simplerPath")
rememberPatchedSvgResource(basePath, simplerPath, loader)
} else {
throw IllegalArgumentException(
"Unable to load '$resourcePath' (base: $basePath), no simpler version available",
e
)
}
}
return remember(resourcePath, density, loader) { painter }
}

View File

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import org.jetbrains.jewel.styling.ButtonStyle
import org.jetbrains.jewel.styling.CheckboxStyle
@@ -61,6 +62,11 @@ interface IntelliJTheme {
@ReadOnlyComposable
get() = LocalTextStyle.current
val contentColor: Color
@Composable
@ReadOnlyComposable
get() = LocalContentColor.current
val isDark: Boolean
@Composable
@ReadOnlyComposable
@@ -172,7 +178,6 @@ interface IntelliJTheme {
}
}
@ExperimentalJewelApi
@Composable
fun IntelliJTheme(
theme: IntelliJThemeDefinition,
@@ -186,13 +191,15 @@ fun IntelliJTheme(
@Composable
fun IntelliJTheme(theme: IntelliJThemeDefinition, content: @Composable () -> Unit) {
val defaultTextStyle = theme.defaultTextStyle
CompositionLocalProvider(
LocalContentColor provides theme.defaultTextStyle.color,
LocalIsDarkTheme provides theme.isDark,
LocalTextStyle provides theme.defaultTextStyle,
LocalContentColor provides defaultTextStyle.color,
LocalTextStyle provides defaultTextStyle,
LocalGlobalColors provides theme.globalColors,
LocalGlobalMetrics provides theme.metrics,
content = content
LocalGlobalMetrics provides theme.globalMetrics,
content = content,
)
}

View File

@@ -1,12 +1,16 @@
package org.jetbrains.jewel
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
@Stable
interface IntelliJThemeColorPalette {
fun lookup(colorKey: String): Color?
}
@Immutable
object EmptyThemeColorPalette : IntelliJThemeColorPalette {
override fun lookup(colorKey: String): Color? = null

View File

@@ -8,7 +8,7 @@ interface IntelliJThemeDefinition {
val isDark: Boolean
val globalColors: GlobalColors
val metrics: GlobalMetrics
val globalMetrics: GlobalMetrics
val defaultTextStyle: TextStyle
val colorPalette: IntelliJThemeColorPalette

View File

@@ -1,11 +1,15 @@
package org.jetbrains.jewel
import androidx.compose.runtime.Immutable
@Immutable
interface IntelliJThemeIconData {
val iconOverrides: Map<String, String>
val colorPalette: Map<String, String>
}
@Immutable
object EmptyThemeIconData : IntelliJThemeIconData {
override val iconOverrides: Map<String, String> = emptyMap()

View File

@@ -0,0 +1,6 @@
package org.jetbrains.jewel
interface JewelResourceLoader {
val searchClasses: List<Class<out Any>>
}

View File

@@ -21,16 +21,23 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.offset
import org.jetbrains.jewel.styling.LabelledTextFieldStyle
/**
* @param placeholder the optional placeholder to be displayed over the component when
* the [value] is empty.
* @param hint the optional hint to be displayed underneath the component. By default it
* will have a greyed out appearance and smaller text.
* @param label the label to display above the component.
*/
@Composable
fun LabelledTextField(
label: @Composable () -> Unit,
value: String,
onValueChange: (String) -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier,
textFieldModifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
isError: Boolean = false,
outline: Outline = Outline.None,
hint: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
@@ -61,7 +68,7 @@ fun LabelledTextField(
textFieldModifier = textFieldModifier,
enabled = enabled,
readOnly = readOnly,
isError = isError,
outline = outline,
hint = hint,
placeholder = placeholder,
trailingIcon = trailingIcon,
@@ -72,10 +79,17 @@ fun LabelledTextField(
onTextLayout = onTextLayout,
style = style,
textStyle = textStyle,
interactionSource = interactionSource
interactionSource = interactionSource,
)
}
/**
* @param placeholder the optional placeholder to be displayed over the component when
* the [value] is empty.
* @param hint the optional hint to be displayed underneath the component. By default it
* will have a greyed out appearance and smaller text.
* @param label the label to display above the component.
*/
@Composable
fun LabelledTextField(
label: @Composable () -> Unit,
@@ -85,7 +99,7 @@ fun LabelledTextField(
textFieldModifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
isError: Boolean = false,
outline: Outline = Outline.None,
hint: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
@@ -104,7 +118,7 @@ fun LabelledTextField(
CompositionLocalProvider(
LocalTextStyle provides style.textStyles.label,
LocalContentColor provides style.colors.label,
content = label
content = label,
)
},
textField = {
@@ -114,7 +128,7 @@ fun LabelledTextField(
modifier = textFieldModifier,
enabled = enabled,
readOnly = readOnly,
isError = isError,
outline = outline,
placeholder = placeholder,
trailingIcon = trailingIcon,
undecorated = undecorated,
@@ -124,7 +138,7 @@ fun LabelledTextField(
onTextLayout = onTextLayout,
style = style,
textStyle = textStyle,
interactionSource = interactionSource
interactionSource = interactionSource,
)
},
hint = hint?.let {
@@ -132,11 +146,11 @@ fun LabelledTextField(
CompositionLocalProvider(
LocalTextStyle provides style.textStyles.hint,
LocalContentColor provides style.colors.hint,
content = it
content = it,
)
}
},
style = style
style = style,
)
}
@@ -164,7 +178,7 @@ private fun LabelledTextFieldLayout(
hint()
}
}
}
},
) { measurables, incomingConstraints ->
val hintMeasurable = measurables.firstOrNull { it.layoutId == HINT_ID }
@@ -173,7 +187,7 @@ private fun LabelledTextFieldLayout(
val constraintsWithoutSpacing = incomingConstraints.offset(
horizontal = -horizontalSpacing,
vertical = -verticalSpacing
vertical = -verticalSpacing,
)
val textFieldPlaceable = measurables.first { it.layoutId == TEXT_FIELD_ID }
@@ -185,7 +199,7 @@ private fun LabelledTextFieldLayout(
val hintPlaceable = hintMeasurable?.measure(
constraintsWithoutSpacing
.offset(vertical = -textFieldPlaceable.height)
.copy(maxWidth = textFieldPlaceable.width)
.copy(maxWidth = textFieldPlaceable.width),
)
val width = labelPlaceable.width + textFieldPlaceable.width + horizontalSpacing
@@ -194,15 +208,15 @@ private fun LabelledTextFieldLayout(
layout(width, height) {
labelPlaceable.placeRelative(
x = 0,
y = Alignment.CenterVertically.align(labelPlaceable.height, textFieldPlaceable.height)
y = Alignment.CenterVertically.align(labelPlaceable.height, textFieldPlaceable.height),
)
textFieldPlaceable.placeRelative(
x = labelPlaceable.width + horizontalSpacing,
y = 0
y = 0,
)
hintPlaceable?.placeRelative(
x = labelPlaceable.width + horizontalSpacing,
y = textFieldPlaceable.height + verticalSpacing
y = textFieldPlaceable.height + verticalSpacing,
)
}
}

View File

@@ -1,12 +1,11 @@
package org.jetbrains.jewel
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.res.ResourceLoader
import org.jetbrains.jewel.foundation.lazy.SelectableLazyItemScope
@@ -52,14 +51,10 @@ fun <T> LazyTree(
onElementDoubleClick = onElementDoubleClick,
interactionSource = interactionSource,
keyActions = keyActions,
chevronContent = { state ->
Box(Modifier.rotate(if (state.isExpanded) 90f else 0f)) {
Icon(
painter = painterResource(style.icons.nodeChevron, resourceLoader),
contentDescription = "Dropdown link",
tint = colors.chevronTintFor(state).value
)
}
chevronContent = { elementState ->
val painterProvider = style.icons.nodeChevron(elementState.isExpanded)
val painter by painterProvider.getPainter(elementState, resourceLoader)
Icon(painter = painter, contentDescription = null)
},
nodeContent = {
CompositionLocalProvider(
@@ -68,12 +63,12 @@ fun <T> LazyTree(
TreeElementState.of(
isFocused,
isSelected,
false
)
false,
),
).value
.takeOrElse { LocalContentColor.current }
)
),
) { nodeContent(it) }
}
},
)
}

View File

@@ -24,6 +24,8 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import org.jetbrains.jewel.styling.HorizontalProgressBarStyle
// TODO implement green/red/yellow variants based on com.intellij.openapi.progress.util.ColorProgressBar
@Composable
fun HorizontalProgressBar(
progress: Float, // from 0 to 1
@@ -48,9 +50,9 @@ fun HorizontalProgressBar(
color = colors.progress,
topLeft = Offset(progressX, 0f),
size = size.copy(width = progressWidth),
cornerRadius = cornerRadius
cornerRadius = cornerRadius,
)
}
},
)
}
@@ -67,13 +69,13 @@ fun IndeterminateHorizontalProgressBar(
targetValue = 2f,
animationSpec = infiniteRepeatable(
tween(durationMillis = cycleDurationMillis, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
repeatMode = RepeatMode.Restart,
),
)
val highlightWidth = style.metrics.indeterminateHighlightWidth
val colors = style.colors
val colorsList by remember { mutableStateOf(listOf(colors.progress, colors.indeterminateHighlight)) }
val colorsList by remember { mutableStateOf(listOf(colors.indeterminateBase, colors.indeterminateHighlight)) }
val shape = RoundedCornerShape(style.metrics.cornerSize)
Box(
@@ -90,9 +92,9 @@ fun IndeterminateHorizontalProgressBar(
colors = colorsList,
start = Offset(x, 0f),
end = Offset(x + highlightWidth.value, 0f),
TileMode.Mirror
)
TileMode.Mirror,
),
)
}
},
)
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.res.ResourceLoader
@@ -85,7 +86,7 @@ fun Link(
indication = indication,
style = style,
resourceLoader = resourceLoader,
icon = null
icon = null,
)
}
@@ -125,7 +126,7 @@ fun ExternalLink(
indication = indication,
style = style,
resourceLoader = resourceLoader,
icon = style.icons.externalLink
icon = style.icons.externalLink,
)
}
@@ -156,7 +157,7 @@ fun DropdownLink(
Box(
Modifier.onHover {
hovered = it
}
},
) {
LinkImpl(
text = text,
@@ -180,7 +181,7 @@ fun DropdownLink(
indication = indication,
style = style,
icon = style.icons.dropdownChevron,
resourceLoader = resourceLoader
resourceLoader = resourceLoader,
)
if (expanded) {
@@ -195,7 +196,8 @@ fun DropdownLink(
modifier = menuModifier,
style = menuStyle,
horizontalAlignment = Alignment.Start, // TODO no idea what goes here
content = menuContent
content = menuContent,
resourceLoader = resourceLoader,
)
}
}
@@ -262,8 +264,8 @@ private fun LinkImpl(
lineHeight = lineHeight,
fontFamily = fontFamily,
fontStyle = fontStyle,
letterSpacing = letterSpacing
)
letterSpacing = letterSpacing,
),
)
val clickable = Modifier.clickable(
@@ -274,28 +276,28 @@ private fun LinkImpl(
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = indication
indication = indication,
)
val focusHaloModifier = Modifier.border(
alignment = Stroke.Alignment.Outside,
width = LocalGlobalMetrics.current.outlineWidth,
color = LocalGlobalColors.current.outlines.focused,
shape = RoundedCornerShape(style.metrics.focusHaloCornerSize)
shape = RoundedCornerShape(style.metrics.focusHaloCornerSize),
)
Row(
modifier = modifier.then(clickable)
.appendIf(linkState.isFocused) { focusHaloModifier },
horizontalArrangement = Arrangement.spacedBy(style.metrics.textIconGap),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
BasicText(
text = text,
style = mergedStyle,
overflow = overflow,
softWrap = true,
maxLines = 1
maxLines = 1,
)
if (icon != null) {
@@ -304,7 +306,7 @@ private fun LinkImpl(
iconPainter,
contentDescription = null,
modifier = Modifier.size(style.metrics.iconSize),
tint = style.colors.iconTint
colorFilter = if (!linkState.isEnabled) ColorFilter.disabled() else null,
)
}
}
@@ -355,7 +357,7 @@ value class LinkState(val state: ULong) : FocusableComponentState {
visited = visited,
pressed = pressed,
hovered = hovered,
active = active
active = active,
)
@Composable
@@ -397,7 +399,7 @@ value class LinkState(val state: ULong) : FocusableComponentState {
(if (focused) Focused else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (active) Active else 0UL)
(if (active) Active else 0UL),
)
}
}

View File

@@ -10,10 +10,12 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
@@ -31,13 +33,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.key.Key
@@ -48,18 +54,24 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalInputModeManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.ResourceLoader
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import org.jetbrains.jewel.CommonStateBitMask.Active
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.border
import org.jetbrains.jewel.foundation.onHover
import org.jetbrains.jewel.styling.MenuItemColors
import org.jetbrains.jewel.styling.MenuItemMetrics
import org.jetbrains.jewel.styling.MenuStyle
@Composable
internal fun MenuContent(
resourceLoader: ResourceLoader,
modifier: Modifier = Modifier,
style: MenuStyle = IntelliJTheme.menuStyle,
content: MenuScope.() -> Unit,
@@ -80,28 +92,28 @@ internal fun MenuContent(
elevation = style.metrics.shadowSize,
shape = menuShape,
ambientColor = colors.shadow,
spotColor = colors.shadow
spotColor = colors.shadow,
)
.border(Stroke.Alignment.Center, style.metrics.borderWidth, colors.border, menuShape)
.background(colors.background, menuShape)
.width(IntrinsicSize.Max)
.onHover {
localMenuManager.onHoveredChange(it)
}
},
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.padding(style.metrics.contentPadding)
.padding(style.metrics.contentPadding),
) {
items.forEach {
when (it) {
is MenuSelectableItem -> {
MenuSelectableItem(
MenuItem(
selected = it.isSelected,
onClick = it.onClick,
enabled = it.isEnabled,
content = it.content
content = it.content,
)
}
@@ -109,7 +121,8 @@ internal fun MenuContent(
MenuSubmenuItem(
enabled = it.isEnabled,
submenu = it.submenu,
content = it.content
content = it.content,
resourceLoader = resourceLoader,
)
}
@@ -122,7 +135,7 @@ internal fun MenuContent(
Box(modifier = Modifier.matchParentSize()) {
VerticalScrollbar(
rememberScrollbarAdapter(scrollState),
modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd)
modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd),
)
}
}
@@ -146,7 +159,7 @@ interface MenuScope {
fun passiveItem(content: @Composable () -> Unit)
}
fun MenuScope.divider() {
fun MenuScope.separator() {
passiveItem {
MenuSeparator()
}
@@ -193,7 +206,7 @@ private fun (MenuScope.() -> Unit).asList() = buildList {
override fun submenu(enabled: Boolean, submenu: MenuScope.() -> Unit, content: @Composable () -> Unit) {
add(SubmenuItem(enabled, submenu, content))
}
}
},
)
}
@@ -222,17 +235,19 @@ private data class SubmenuItem(
@Composable
fun MenuSeparator(
modifier: Modifier = Modifier,
style: MenuStyle = IntelliJTheme.menuStyle,
metrics: MenuItemMetrics = IntelliJTheme.menuStyle.metrics.itemMetrics,
colors: MenuItemColors = IntelliJTheme.menuStyle.colors.itemColors,
) {
Divider(
modifier = modifier.padding(style.metrics.itemMetrics.separatorPadding),
modifier = modifier.padding(metrics.separatorPadding),
thickness = metrics.separatorThickness,
orientation = Orientation.Horizontal,
color = style.colors.itemColors.separator
color = colors.separator,
)
}
@Composable
fun MenuSelectableItem(
fun MenuItem(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@@ -283,9 +298,9 @@ fun MenuSelectableItem(
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
indication = null,
)
.fillMaxWidth()
.fillMaxWidth(),
) {
DisposableEffect(Unit) {
if (selected) {
@@ -295,20 +310,19 @@ fun MenuSelectableItem(
onDispose { }
}
val colors = style.colors.itemColors
val metrics = style.metrics
val shape = RoundedCornerShape(metrics.itemMetrics.cornerSize)
val itemColors = style.colors.itemColors
val itemMetrics = style.metrics.itemMetrics
CompositionLocalProvider(
LocalContentColor provides colors.contentFor(itemState).value
LocalContentColor provides itemColors.contentFor(itemState).value,
) {
Row(
val backgroundColor by itemColors.backgroundFor(itemState)
Box(
Modifier
.padding(metrics.itemMetrics.padding)
.background(colors.backgroundFor(itemState).value, shape)
.fillMaxWidth()
.padding(metrics.itemMetrics.contentPadding),
verticalAlignment = Alignment.CenterVertically
.drawItemBackground(itemMetrics, backgroundColor)
.padding(itemMetrics.contentPadding),
) {
content()
}
@@ -318,6 +332,7 @@ fun MenuSelectableItem(
@Composable
fun MenuSubmenuItem(
resourceLoader: ResourceLoader,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -352,8 +367,14 @@ fun MenuSubmenuItem(
}
}
val itemColors = style.colors.itemColors
val menuMetrics = style.metrics
val backgroundColor by itemColors.backgroundFor(itemState)
Box(
modifier = modifier
.fillMaxWidth()
.drawItemBackground(menuMetrics.itemMetrics, backgroundColor)
.focusRequester(focusRequester)
.clickable(
onClick = {
@@ -362,7 +383,7 @@ fun MenuSubmenuItem(
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null
indication = null,
)
.onKeyEvent {
if (it.type == KeyEventType.KeyDown && it.key == Key.DirectionRight) {
@@ -371,34 +392,27 @@ fun MenuSubmenuItem(
} else {
false
}
}
.fillMaxWidth()
},
) {
val colors = style.colors.itemColors
val metrics = style.metrics
val shape = RoundedCornerShape(metrics.itemMetrics.cornerSize)
CompositionLocalProvider(
LocalContentColor provides colors.contentFor(itemState).value
LocalContentColor provides itemColors.contentFor(itemState).value,
) {
Row(
Modifier
.padding(metrics.itemMetrics.padding)
.background(colors.backgroundFor(itemState).value, shape)
.fillMaxWidth()
.padding(metrics.submenuMetrics.itemPadding),
verticalAlignment = Alignment.CenterVertically
Modifier.fillMaxWidth()
.padding(menuMetrics.itemMetrics.contentPadding),
verticalAlignment = Alignment.CenterVertically,
) {
Box(Modifier.weight(1f)) {
content()
}
Box(Modifier.width(24.dp), contentAlignment = Alignment.Center) {
Icon(
painter = painterResource(style.icons.submenuChevron),
tint = colors.iconTintFor(itemState).value
)
}
val chevronPainter by style.icons.submenuChevron.getPainter(itemState, resourceLoader)
Icon(
painter = chevronPainter,
tint = itemColors.iconTintFor(itemState).value,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
}
}
@@ -413,14 +427,42 @@ fun MenuSubmenuItem(
}
},
style = style,
content = submenu
content = submenu,
resourceLoader = resourceLoader,
)
}
}
}
private fun Modifier.drawItemBackground(itemMetrics: MenuItemMetrics, backgroundColor: Color) =
drawBehind {
val cornerSizePx = itemMetrics.selectionCornerSize.toPx(size, density = this)
val cornerRadius = CornerRadius(cornerSizePx, cornerSizePx)
val outerPadding = itemMetrics.outerPadding
val offset = Offset(
x = outerPadding.calculateLeftPadding(layoutDirection).toPx(),
y = outerPadding.calculateTopPadding().toPx(),
)
drawRoundRect(
color = backgroundColor,
cornerRadius = cornerRadius,
topLeft = offset,
size = size.subtract(outerPadding, density = this, layoutDirection),
)
}
private fun Size.subtract(paddingValues: PaddingValues, density: Density, layoutDirection: LayoutDirection): Size =
with(density) {
Size(
width - paddingValues.calculateLeftPadding(layoutDirection).toPx() - paddingValues.calculateRightPadding(layoutDirection).toPx(),
height - paddingValues.calculateTopPadding().toPx() - paddingValues.calculateBottomPadding().toPx(),
)
}
@Composable
internal fun Submenu(
resourceLoader: ResourceLoader,
onDismissRequest: (InputMode) -> Boolean,
modifier: Modifier = Modifier,
style: MenuStyle = IntelliJTheme.menuStyle,
@@ -430,9 +472,9 @@ internal fun Submenu(
val popupPositionProvider = AnchorHorizontalMenuPositionProvider(
contentOffset = style.metrics.submenuMetrics.offset,
contentMargin = style.metrics.margin,
contentMargin = style.metrics.menuMargin,
alignment = Alignment.Top,
density = density
density = density,
)
var focusManager: FocusManager? by mutableStateOf(null)
@@ -443,16 +485,17 @@ internal fun Submenu(
}
Popup(
focusable = true,
popupPositionProvider = popupPositionProvider,
onDismissRequest = {
menuManager.closeAll(InputMode.Touch, false)
},
popupPositionProvider = popupPositionProvider,
properties = PopupProperties(focusable = true),
onPreviewKeyEvent = { false },
onKeyEvent = {
val currentFocusManager = checkNotNull(focusManager) { "FocusManager must not be null" }
val currentInputModeManager = checkNotNull(inputModeManager) { "InputModeManager must not be null" }
handlePopupMenuOnKeyEvent(it, currentFocusManager, currentInputModeManager, menuManager)
}
},
) {
focusManager = LocalFocusManager.current
inputModeManager = LocalInputModeManager.current
@@ -460,7 +503,8 @@ internal fun Submenu(
CompositionLocalProvider(LocalMenuManager provides menuManager) {
MenuContent(
modifier = modifier,
content = content
content = content,
resourceLoader = resourceLoader,
)
}
}
@@ -535,50 +579,3 @@ value class MenuItemState(val state: ULong) : SelectableComponentState {
}
}
}
class MenuManager(
val onDismissRequest: (InputMode) -> Boolean,
private val parentMenuManager: MenuManager? = null,
) {
private var isHovered: Boolean = false
/**
* Called when the hovered state of the menu changes.
* This is used to abort parent menu closing in unforced mode
* when submenu closed by click parent menu's item.
*
* @param hovered true if the menu is hovered, false otherwise.
*/
internal fun onHoveredChange(hovered: Boolean) {
isHovered = hovered
}
/**
* Close all menus in the hierarchy.
*
* @param mode the input mode, menus close by pointer or keyboard event.
* @param force true to force close all menus ignore parent hover state, false otherwise.
*/
fun closeAll(mode: InputMode, force: Boolean) {
// We ignore the pointer event if the menu is hovered in unforced mode.
if (!force && mode == InputMode.Touch && isHovered) return
if (onDismissRequest(mode)) {
parentMenuManager?.closeAll(mode, force)
}
}
fun close(mode: InputMode) = onDismissRequest(mode)
fun isRootMenu(): Boolean = parentMenuManager == null
fun isSubmenu(): Boolean = parentMenuManager != null
fun submenuManager(onDismissRequest: (InputMode) -> Boolean) =
MenuManager(onDismissRequest = onDismissRequest, parentMenuManager = this)
}
val LocalMenuManager = staticCompositionLocalOf<MenuManager> {
error("No MenuManager provided")
}

View File

@@ -0,0 +1,51 @@
package org.jetbrains.jewel
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.input.InputMode
class MenuManager(
val onDismissRequest: (InputMode) -> Boolean,
private val parentMenuManager: MenuManager? = null,
) {
private var isHovered: Boolean = false
/**
* Called when the hovered state of the menu changes.
* This is used to abort parent menu closing in unforced mode
* when submenu closed by click parent menu's item.
*
* @param hovered true if the menu is hovered, false otherwise.
*/
internal fun onHoveredChange(hovered: Boolean) {
isHovered = hovered
}
/**
* Close all menus in the hierarchy.
*
* @param mode the input mode, menus close by pointer or keyboard event.
* @param force true to force close all menus ignore parent hover state, false otherwise.
*/
fun closeAll(mode: InputMode, force: Boolean) {
// We ignore the pointer event if the menu is hovered in unforced mode.
if (!force && mode == InputMode.Touch && isHovered) return
if (onDismissRequest(mode)) {
parentMenuManager?.closeAll(mode, force)
}
}
fun close(mode: InputMode) = onDismissRequest(mode)
fun isRootMenu(): Boolean = parentMenuManager == null
fun isSubmenu(): Boolean = parentMenuManager != null
fun submenuManager(onDismissRequest: (InputMode) -> Boolean) =
MenuManager(onDismissRequest = onDismissRequest, parentMenuManager = this)
}
val LocalMenuManager = staticCompositionLocalOf<MenuManager> {
error("No MenuManager provided")
}

View File

@@ -1,6 +1,5 @@
package org.jetbrains.jewel
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
@@ -29,10 +28,10 @@ enum class Outline {
@Composable
fun Modifier.focusOutline(
state: FocusableComponentState,
outlineShape: Shape = RoundedCornerShape(LocalGlobalMetrics.current.outlineCornerSize),
outlineWidth: Dp = LocalGlobalMetrics.current.outlineWidth,
outlineShape: Shape,
outlineWidth: Dp = IntelliJTheme.globalMetrics.outlineWidth,
): Modifier {
val outlineColors = LocalGlobalColors.current.outlines
val outlineColors = IntelliJTheme.globalColors.outlines
return thenIf(state.isFocused) {
val outlineColor = outlineColors.focused
@@ -40,34 +39,15 @@ fun Modifier.focusOutline(
}
}
@Composable
fun Modifier.outline(
state: StateWithOutline,
outlineShape: Shape = RoundedCornerShape(LocalGlobalMetrics.current.outlineCornerSize),
outlineWidth: Dp = LocalGlobalMetrics.current.outlineWidth,
): Modifier {
val outlineColors = LocalGlobalColors.current.outlines
return thenIf(state.hasOutline) {
val outlineColor = when {
state.isError -> outlineColors.focusedError
state.isWarning -> outlineColors.focusedWarning
state.isFocused -> outlineColors.focused
else -> error("State $state says it has an outline, but doesn't really")
}
border(Stroke.Alignment.Inside, outlineWidth, outlineColor, outlineShape)
}
}
@Composable
fun Modifier.outline(
state: FocusableComponentState,
outline: Outline,
outlineShape: Shape,
alignment: Stroke.Alignment = Stroke.Alignment.Outside,
outlineShape: Shape = RoundedCornerShape(LocalGlobalMetrics.current.outlineCornerSize),
outlineWidth: Dp = LocalGlobalMetrics.current.outlineWidth,
outlineWidth: Dp = IntelliJTheme.globalMetrics.outlineWidth,
): Modifier {
val outlineColors = LocalGlobalColors.current.outlines
val outlineColors = IntelliJTheme.globalColors.outlines
return thenIf(outline != Outline.None) {
val outlineColor = when {

View File

@@ -83,7 +83,7 @@ internal data class AnchorVerticalMenuPositionProvider(
left = leftMargin,
top = topMargin,
right = windowSize.width - rightMargin,
bottom = windowSize.height - bottomMargin
bottom = windowSize.height - bottomMargin,
)
// The content offset specified using the dropdown offset parameter.
@@ -136,7 +136,7 @@ internal data class AnchorHorizontalMenuPositionProvider(
left = leftMargin,
top = topMargin,
right = windowSize.width - rightMargin,
bottom = windowSize.height - bottomMargin
bottom = windowSize.height - bottomMargin,
)
// The content offset specified using the dropdown offset parameter.

View File

@@ -58,7 +58,7 @@ fun RadioButton(
interactionSource = interactionSource,
style = style,
textStyle = textStyle,
content = null
content = null,
)
}
@@ -84,7 +84,7 @@ fun RadioButtonRow(
resourceLoader = resourceLoader,
interactionSource = interactionSource,
style = style,
textStyle = textStyle
textStyle = textStyle,
) {
Text(text)
}
@@ -113,7 +113,7 @@ fun RadioButtonRow(
style = style,
textStyle = textStyle,
resourceLoader = resourceLoader,
content = content
content = content,
)
}
@@ -159,7 +159,7 @@ private fun RadioButtonImpl(
enabled = enabled,
role = Role.RadioButton,
interactionSource = interactionSource,
indication = null
indication = null,
)
val colors = style.colors
@@ -174,14 +174,14 @@ private fun RadioButtonImpl(
Row(
wrapperModifier,
horizontalArrangement = Arrangement.spacedBy(metrics.iconContentGap),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
RadioButtonImage(Modifier, radioButtonPainter, radioButtonModifier)
val contentColor by colors.contentFor(radioButtonState)
CompositionLocalProvider(
LocalTextStyle provides textStyle.copy(color = contentColor.takeOrElse { textStyle.color }),
LocalContentColor provides contentColor.takeOrElse { textStyle.color }
LocalContentColor provides contentColor.takeOrElse { textStyle.color },
) {
content()
}
@@ -238,7 +238,7 @@ value class RadioButtonState(val state: ULong) : SelectableComponentState {
focused = focused,
pressed = pressed,
hovered = hovered,
active = active
active = active,
)
override fun toString() =
@@ -264,7 +264,7 @@ value class RadioButtonState(val state: ULong) : SelectableComponentState {
(if (focused) Focused else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (active) Active else 0UL)
(if (active) Active else 0UL),
)
}
}

View File

@@ -0,0 +1,8 @@
package org.jetbrains.jewel
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.res.ResourceLoader
val LocalResourceLoader = staticCompositionLocalOf<ResourceLoader> {
ResourceLoader.Default
}

View File

@@ -36,15 +36,15 @@ fun VerticalScrollbar(
shape = shape,
hoverDurationMillis = hoverDurationMillis,
unhoverColor = style.colors.thumbBackground,
hoverColor = style.colors.thumbBackgroundHovered
)
hoverColor = style.colors.thumbBackgroundHovered,
),
) {
VerticalScrollbar(
adapter = adapter,
modifier = modifier.padding(style.metrics.trackPadding),
reverseLayout = reverseLayout,
style = LocalScrollbarStyle.current,
interactionSource = interactionSource
interactionSource = interactionSource,
)
}
}
@@ -67,15 +67,15 @@ fun HorizontalScrollbar(
shape = shape,
hoverDurationMillis = hoverDurationMillis,
unhoverColor = style.colors.thumbBackground,
hoverColor = style.colors.thumbBackgroundHovered
)
hoverColor = style.colors.thumbBackgroundHovered,
),
) {
HorizontalScrollbar(
adapter = adapter,
modifier = modifier.padding(style.metrics.trackPadding),
reverseLayout = reverseLayout,
style = LocalScrollbarStyle.current,
interactionSource = interactionSource
interactionSource = interactionSource,
)
}
}
@@ -98,15 +98,15 @@ fun TabStripHorizontalScrollbar(
shape = shape,
hoverDurationMillis = hoverDurationMillis,
unhoverColor = style.colors.thumbBackground,
hoverColor = style.colors.thumbBackgroundHovered
)
hoverColor = style.colors.thumbBackgroundHovered,
),
) {
HorizontalScrollbar(
adapter = adapter,
modifier = modifier.padding(1.dp),
reverseLayout = reverseLayout,
style = LocalScrollbarStyle.current,
interactionSource = interactionSource
interactionSource = interactionSource,
)
}
}

View File

@@ -1,47 +0,0 @@
package org.jetbrains.jewel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
interface StateWithOutline : FocusableComponentState {
@Stable
val isError: Boolean
@Stable
val isWarning: Boolean
@Stable
val hasOutline: Boolean
get() = outline != Outline.None
@Stable
val outline: Outline
get() = when {
isError -> Outline.Error
isWarning -> Outline.Warning
else -> Outline.None
}
@Composable
fun <T> chooseValueWithOutline(
normal: T,
disabled: T,
focused: T,
pressed: T,
hovered: T,
warning: T,
error: T,
active: T,
): T =
when {
!isEnabled -> disabled
isPressed && !IntelliJTheme.isSwingCompatMode -> pressed
isHovered && !IntelliJTheme.isSwingCompatMode -> hovered
isFocused -> focused
isError -> error
isWarning -> warning
isActive -> active
else -> normal
}
}

View File

@@ -42,7 +42,7 @@ fun TabStrip(
Box(
modifier
.focusable(true, remember { MutableInteractionSource() })
.onHover { tabStripState = tabStripState.copy(hovered = it) }
.onHover { tabStripState = tabStripState.copy(hovered = it) },
) {
Row(
modifier = Modifier
@@ -52,12 +52,12 @@ fun TabStrip(
reverseDirection = ScrollableDefaults.reverseDirection(
LocalLayoutDirection.current,
Orientation.Vertical,
false
false,
),
state = scrollState,
interactionSource = remember { MutableInteractionSource() }
interactionSource = remember { MutableInteractionSource() },
)
.selectableGroup()
.selectableGroup(),
) {
tabs.forEach {
TabImpl(isActive = tabStripState.isActive, tabData = it)
@@ -69,20 +69,20 @@ fun TabStrip(
animationSpec = tween(
durationMillis = 125,
delayMillis = 0,
easing = LinearEasing
)
easing = LinearEasing,
),
),
exit = fadeOut(
animationSpec = tween(
durationMillis = 125,
delayMillis = 700,
easing = LinearEasing
)
)
easing = LinearEasing,
),
),
) {
TabStripHorizontalScrollbar(
adapter = rememberScrollbarAdapter(scrollState),
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
)
}
}
@@ -208,7 +208,7 @@ value class TabStripState(val state: ULong) : FocusableComponentState {
focused = focused,
pressed = pressed,
hovered = hovered,
active = active
active = active,
)
override fun toString() =
@@ -232,7 +232,7 @@ value class TabStripState(val state: ULong) : FocusableComponentState {
(if (pressed) CommonStateBitMask.Pressed else 0UL) or
(if (warning) CommonStateBitMask.Warning else 0UL) or
(if (error) CommonStateBitMask.Error else 0UL) or
(if (active) CommonStateBitMask.Active else 0UL)
(if (active) CommonStateBitMask.Active else 0UL),
)
}
}

View File

@@ -78,7 +78,7 @@ internal fun TabImpl(
CompositionLocalProvider(
LocalIndication provides NoIndication,
LocalContentColor provides tabStyle.colors.contentFor(tabState).value
LocalContentColor provides tabStyle.colors.contentFor(tabState).value,
) {
val labelAlpha by tabStyle.contentAlpha.labelFor(tabState)
val iconAlpha by tabStyle.contentAlpha.iconFor(tabState)
@@ -92,7 +92,7 @@ internal fun TabImpl(
selected = tabData.selected,
interactionSource = interactionSource,
indication = NoIndication,
role = Role.Tab
role = Role.Tab,
)
.drawBehind {
val strokeThickness = lineThickness.toPx()
@@ -105,12 +105,12 @@ internal fun TabImpl(
start = Offset(0 + capDxFix, startY),
end = Offset(endX - capDxFix, startY),
strokeWidth = strokeThickness,
cap = StrokeCap.Round
cap = StrokeCap.Round,
)
}
.padding(tabStyle.metrics.tabPadding),
horizontalArrangement = Arrangement.spacedBy(tabStyle.metrics.closeContentGap),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
) {
tabData.tabIconResource?.let { icon ->
val iconPainter = painterResource(icon, LocalResourceLoader.current)
@@ -120,7 +120,7 @@ internal fun TabImpl(
Text(
modifier = Modifier.alpha(labelAlpha),
text = tabData.label,
color = tabStyle.colors.contentFor(tabState).value
color = tabStyle.colors.contentFor(tabState).value,
)
val showCloseIcon = when (tabData) {
is TabData.Default -> tabData.closable
@@ -147,10 +147,10 @@ internal fun TabImpl(
interactionSource = closeActionInteractionSource,
indication = null,
onClick = tabData.onClose,
role = Role.Button
role = Role.Button,
).size(16.dp),
painter = closePainter,
contentDescription = "Close tab ${tabData.label}"
contentDescription = "Close tab ${tabData.label}",
)
} else if (tabData.closable) {
Spacer(Modifier.size(16.dp))
@@ -215,7 +215,7 @@ value class TabState(val state: ULong) : SelectableComponentState {
focused = focused,
pressed = pressed,
hovered = hovered,
active = active
active = active,
)
override fun toString() =
@@ -237,7 +237,7 @@ value class TabState(val state: ULong) : SelectableComponentState {
(if (focused) Focused else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (active) Active else 0UL)
(if (active) Active else 0UL),
)
}
}

View File

@@ -3,7 +3,6 @@ package org.jetbrains.jewel
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -36,7 +35,7 @@ fun Text(
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
style: TextStyle = IntelliJTheme.defaultTextStyle,
) {
Text(
AnnotatedString(text),
@@ -55,7 +54,7 @@ fun Text(
maxLines,
emptyMap(),
onTextLayout,
style
style,
)
}
@@ -77,7 +76,7 @@ fun Text(
maxLines: Int = Int.MAX_VALUE,
inlineContent: Map<String, InlineTextContent> = emptyMap(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
style: TextStyle = IntelliJTheme.defaultTextStyle,
) {
val textColor = color.takeOrElse {
style.color.takeOrElse {
@@ -97,8 +96,8 @@ fun Text(
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
)
letterSpacing = letterSpacing,
),
)
BasicText(text, modifier, mergedStyle, onTextLayout, overflow, softWrap, maxLines, minLines = 1, inlineContent)
}
@@ -110,9 +109,3 @@ val LocalTextStyle = staticCompositionLocalOf<TextStyle> {
val LocalContentColor = staticCompositionLocalOf<Color> {
error("No ContentColor provided")
}
@Composable
fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
val mergedStyle = LocalTextStyle.current.merge(value)
CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
}

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
@@ -24,12 +23,13 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.offset
import org.jetbrains.jewel.styling.TextAreaStyle
import kotlin.math.max
/**
* @param placeholder the optional placeholder to be displayed over the
* component when the [value] is empty.
*/
@Composable
fun TextArea(
value: String,
@@ -37,9 +37,8 @@ fun TextArea(
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
isError: Boolean = false,
outline: Outline = Outline.None,
placeholder: @Composable (() -> Unit)? = null,
hint: @Composable (() -> Unit)? = null,
undecorated: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
@@ -69,9 +68,8 @@ fun TextArea(
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
isError = isError,
outline = outline,
placeholder = placeholder,
hint = hint,
undecorated = undecorated,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
@@ -80,10 +78,14 @@ fun TextArea(
onTextLayout = onTextLayout,
style = style,
textStyle = textStyle,
interactionSource = interactionSource
interactionSource = interactionSource,
)
}
/**
* @param placeholder the optional placeholder to be displayed over the
* component when the [value] is empty.
*/
@Composable
fun TextArea(
value: TextFieldValue,
@@ -92,9 +94,8 @@ fun TextArea(
enabled: Boolean = true,
readOnly: Boolean = false,
placeholder: @Composable (() -> Unit)? = null,
hint: @Composable (() -> Unit)? = null,
undecorated: Boolean = false,
isError: Boolean = false,
outline: Outline = Outline.None,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
@@ -104,13 +105,15 @@ fun TextArea(
textStyle: TextStyle = IntelliJTheme.defaultTextStyle,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val minSize = style.metrics.minSize
InputField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
modifier = modifier
.defaultMinSize(minWidth = minSize.width, minHeight = minSize.height),
enabled = enabled,
readOnly = readOnly,
isError = isError,
outline = outline,
undecorated = undecorated,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
@@ -120,108 +123,89 @@ fun TextArea(
onTextLayout = onTextLayout,
style = style,
textStyle = textStyle,
interactionSource = interactionSource
interactionSource = interactionSource,
) { innerTextField, state ->
val minSize = style.metrics.minSize
TextAreaDecorationBox(
modifier = Modifier
.defaultMinSize(minWidth = minSize.width, minHeight = minSize.height),
innerTextField = innerTextField,
contentPadding = style.metrics.contentPadding,
placeholderTextColor = style.colors.placeholder,
placeholder = if (value.text.isEmpty()) placeholder else null,
hintTextStyle = style.hintTextStyle,
hintTextColor = style.colors.hintContentFor(state).value,
hint = hint
textStyle = textStyle,
)
}
}
@Composable
private fun TextAreaDecorationBox(
modifier: Modifier = Modifier,
innerTextField: @Composable () -> Unit,
contentPadding: PaddingValues,
textStyle: TextStyle,
placeholderTextColor: Color,
placeholder: @Composable (() -> Unit)?,
hintTextStyle: TextStyle,
hintTextColor: Color,
hint: @Composable (() -> Unit)?,
) {
Layout(
modifier = modifier,
content = {
if (placeholder != null) {
Box(modifier = Modifier.layoutId(PLACEHOLDER_ID), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier.layoutId(PLACEHOLDER_ID),
contentAlignment = Alignment.CenterStart,
) {
CompositionLocalProvider(
LocalTextStyle provides textStyle.copy(color = placeholderTextColor),
LocalContentColor provides placeholderTextColor,
content = placeholder
content = placeholder,
)
}
}
Box(modifier = Modifier.layoutId(TEXT_FIELD_ID), propagateMinConstraints = true) {
Box(
modifier = Modifier.layoutId(TEXT_AREA_ID),
contentAlignment = Alignment.CenterStart,
propagateMinConstraints = true,
) {
innerTextField()
}
if (hint != null) {
Box(Modifier.layoutId(HINT_ID).fillMaxWidth()) {
CompositionLocalProvider(
LocalTextStyle provides hintTextStyle,
LocalContentColor provides hintTextColor,
content = hint
)
}
}
}
},
) { measurables, incomingConstraints ->
val horizontalPadding =
(contentPadding.calculateLeftPadding(layoutDirection) + contentPadding.calculateRightPadding(layoutDirection)).roundToPx()
(
contentPadding.calculateLeftPadding(layoutDirection) +
contentPadding.calculateRightPadding(layoutDirection)
).roundToPx()
val verticalPadding =
(contentPadding.calculateTopPadding() + contentPadding.calculateBottomPadding()).roundToPx()
(
contentPadding.calculateTopPadding() +
contentPadding.calculateBottomPadding()
).roundToPx()
// measure hint
val hintConstraints = incomingConstraints.copy(minHeight = 0)
val hintPlaceable = measurables.find { it.layoutId == HINT_ID }?.measure(hintConstraints)
val occupiedSpaceVertically = hintPlaceable?.height ?: 0
val textAreaConstraints = incomingConstraints.offset(
horizontal = -horizontalPadding,
vertical = -verticalPadding,
).copy(minHeight = 0)
val constraintsWithoutPadding = incomingConstraints.offset(
-horizontalPadding,
-verticalPadding - occupiedSpaceVertically
)
val textAreaPlaceable = measurables.first { it.layoutId == TEXT_AREA_ID }.measure(textAreaConstraints)
val textConstraints = constraintsWithoutPadding
val textFieldPlaceable = measurables.first { it.layoutId == TEXT_FIELD_ID }.measure(textConstraints)
// measure placeholder
val placeholderConstraints = textConstraints.copy(minWidth = 0, minHeight = 0)
// Measure placeholder
val placeholderConstraints = textAreaConstraints.copy(minWidth = 0, minHeight = 0)
val placeholderPlaceable = measurables.find { it.layoutId == PLACEHOLDER_ID }?.measure(placeholderConstraints)
val width = calculateWidth(
textFieldPlaceable,
textAreaPlaceable,
placeholderPlaceable,
horizontalPadding,
hintPlaceable,
constraintsWithoutPadding
textAreaConstraints,
)
val height = calculateHeight(
textFieldPlaceable,
textAreaPlaceable,
placeholderPlaceable,
verticalPadding,
hintPlaceable,
constraintsWithoutPadding
textAreaConstraints,
)
layout(width, height) {
place(
height,
contentPadding,
hintPlaceable,
textFieldPlaceable,
textAreaPlaceable,
placeholderPlaceable,
layoutDirection,
this@Layout
)
}
}
@@ -230,55 +214,44 @@ private fun TextAreaDecorationBox(
private fun calculateWidth(
textFieldPlaceable: Placeable,
placeholderPlaceable: Placeable?,
horizontalPadding: Int,
hintPlaceable: Placeable?,
constraints: Constraints,
): Int {
return maxOf(
textFieldPlaceable.width + horizontalPadding,
(placeholderPlaceable?.width ?: 0) + horizontalPadding,
hintPlaceable?.width ?: 0,
constraints.minWidth
): Int =
maxOf(
textFieldPlaceable.width,
placeholderPlaceable?.width ?: 0,
)
}
.coerceAtLeast(constraints.minWidth)
private fun calculateHeight(
textFieldPlaceable: Placeable,
placeholderPlaceable: Placeable?,
verticalPadding: Int,
hintPlaceable: Placeable?,
constraints: Constraints,
): Int {
val middleSection = maxOf(
val textAreaHeight = maxOf(
textFieldPlaceable.height,
placeholderPlaceable?.height ?: 0
) + verticalPadding
val wrappedHeight = (hintPlaceable?.height ?: 0) + middleSection
return max(wrappedHeight, constraints.minHeight)
placeholderPlaceable?.height ?: 0,
)
return (textAreaHeight + verticalPadding).coerceAtLeast(constraints.minHeight)
}
private fun Placeable.PlacementScope.place(
height: Int,
contentPadding: PaddingValues,
hintPlaceable: Placeable?,
textFieldPlaceable: Placeable,
textAreaPlaceable: Placeable,
placeholderPlaceable: Placeable?,
layoutDirection: LayoutDirection,
density: Density,
) = with(density) {
hintPlaceable?.placeRelative(
) {
// placed center vertically
textAreaPlaceable.placeRelative(
0,
height - hintPlaceable.height
Alignment.CenterVertically.align(textAreaPlaceable.height, height),
)
val y = contentPadding.calculateTopPadding().roundToPx()
val x = contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
textFieldPlaceable.placeRelative(x, y)
placeholderPlaceable?.placeRelative(x, y)
// placed similar to the input text above
placeholderPlaceable?.placeRelative(
0,
Alignment.CenterVertically.align(placeholderPlaceable.height, height),
)
}
private const val PLACEHOLDER_ID = "Placeholder"
private const val TEXT_FIELD_ID = "TextField"
private const val HINT_ID = "Hint"
private const val TEXT_AREA_ID = "TextField"

View File

@@ -23,11 +23,14 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.offset
import org.jetbrains.jewel.styling.TextFieldStyle
import kotlin.math.max
/**
* @param placeholder the optional placeholder to be displayed over the
* component when the [value] is empty.
*/
@Composable
fun TextField(
value: String,
@@ -35,7 +38,7 @@ fun TextField(
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
isError: Boolean = false,
outline: Outline = Outline.None,
placeholder: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
undecorated: Boolean = false,
@@ -65,7 +68,7 @@ fun TextField(
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
isError = isError,
outline = outline,
placeholder = placeholder,
trailingIcon = trailingIcon,
undecorated = undecorated,
@@ -74,10 +77,14 @@ fun TextField(
keyboardActions = keyboardActions,
onTextLayout = onTextLayout,
style = style,
interactionSource = interactionSource
interactionSource = interactionSource,
)
}
/**
* @param placeholder the optional placeholder to be displayed over the
* component when the [value] is empty.
*/
@Composable
fun TextField(
value: TextFieldValue,
@@ -85,7 +92,7 @@ fun TextField(
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
isError: Boolean = false,
outline: Outline = Outline.None,
placeholder: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
undecorated: Boolean = false,
@@ -103,7 +110,7 @@ fun TextField(
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
isError = isError,
outline = outline,
undecorated = undecorated,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
@@ -113,17 +120,18 @@ fun TextField(
onTextLayout = onTextLayout,
style = style,
textStyle = textStyle,
interactionSource = interactionSource
interactionSource = interactionSource,
) { innerTextField, _ ->
val minSize = style.metrics.minSize
TextFieldDecorationBox(
modifier = Modifier.defaultMinSize(minHeight = minSize.width, minWidth = minSize.height)
modifier = Modifier.defaultMinSize(minWidth = minSize.width, minHeight = minSize.height)
.padding(style.metrics.contentPadding),
innerTextField = innerTextField,
textStyle = textStyle,
placeholderTextColor = style.colors.placeholder,
placeholder = if (value.text.isEmpty()) placeholder else null,
trailingIcon = trailingIcon
trailingIcon = trailingIcon,
)
}
}
@@ -132,6 +140,7 @@ fun TextField(
private fun TextFieldDecorationBox(
modifier: Modifier = Modifier,
innerTextField: @Composable () -> Unit,
textStyle: TextStyle,
placeholderTextColor: Color,
placeholder: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
@@ -147,8 +156,9 @@ private fun TextFieldDecorationBox(
if (placeholder != null) {
Box(modifier = Modifier.layoutId(PLACEHOLDER_ID), contentAlignment = Alignment.Center) {
CompositionLocalProvider(
LocalTextStyle provides textStyle.copy(color = placeholderTextColor),
LocalContentColor provides placeholderTextColor,
content = placeholder
content = placeholder,
)
}
}
@@ -156,40 +166,37 @@ private fun TextFieldDecorationBox(
Box(modifier = Modifier.layoutId(TEXT_FIELD_ID), propagateMinConstraints = true) {
innerTextField()
}
}
},
) { measurables, incomingConstraints ->
// used to calculate the constraints for measuring elements that will be placed in a row
var occupiedSpaceHorizontally = 0
val constraintsWithoutPadding = incomingConstraints
val iconsConstraints = constraintsWithoutPadding.copy(minWidth = 0, minHeight = 0)
val iconConstraints = incomingConstraints.copy(minWidth = 0, minHeight = 0)
// measure trailing icon
val trailingPlaceable = measurables.find { it.layoutId == TRAILING_ID }
?.measure(iconsConstraints)
?.measure(iconConstraints)
occupiedSpaceHorizontally += trailingPlaceable?.width ?: 0
val textConstraints = constraintsWithoutPadding.offset(
horizontal = -occupiedSpaceHorizontally
val textFieldConstraints = incomingConstraints.offset(
horizontal = -occupiedSpaceHorizontally,
).copy(minHeight = 0)
val textFieldPlaceable = measurables.first { it.layoutId == TEXT_FIELD_ID }.measure(textConstraints)
val textFieldPlaceable = measurables.first { it.layoutId == TEXT_FIELD_ID }.measure(textFieldConstraints)
// measure placeholder
val placeholderConstraints = textConstraints.copy(minWidth = 0)
val placeholderConstraints = textFieldConstraints.copy(minWidth = 0)
val placeholderPlaceable = measurables.find { it.layoutId == PLACEHOLDER_ID }?.measure(placeholderConstraints)
val width = calculateWidth(
trailingPlaceable,
textFieldPlaceable,
placeholderPlaceable,
incomingConstraints
incomingConstraints,
)
val height = calculateHeight(
trailingPlaceable,
textFieldPlaceable,
placeholderPlaceable,
incomingConstraints
incomingConstraints,
)
layout(width, height) {
@@ -199,7 +206,6 @@ private fun TextFieldDecorationBox(
trailingPlaceable,
textFieldPlaceable,
placeholderPlaceable,
this@Layout
)
}
}
@@ -213,7 +219,7 @@ private fun calculateWidth(
): Int {
val middleSection = maxOf(
textFieldPlaceable.width,
placeholderPlaceable?.width ?: 0
placeholderPlaceable?.width ?: 0,
)
val wrappedWidth = middleSection + (trailingPlaceable?.width ?: 0)
return max(wrappedWidth, constraints.minWidth)
@@ -224,14 +230,12 @@ private fun calculateHeight(
textFieldPlaceable: Placeable,
placeholderPlaceable: Placeable?,
constraints: Constraints,
): Int {
return maxOf(
textFieldPlaceable.height,
placeholderPlaceable?.height ?: 0,
trailingPlaceable?.height ?: 0,
constraints.minHeight
)
}
): Int = maxOf(
textFieldPlaceable.height,
placeholderPlaceable?.height ?: 0,
trailingPlaceable?.height ?: 0,
constraints.minHeight,
)
private fun Placeable.PlacementScope.place(
height: Int,
@@ -239,28 +243,24 @@ private fun Placeable.PlacementScope.place(
trailingPlaceable: Placeable?,
textFieldPlaceable: Placeable,
placeholderPlaceable: Placeable?,
density: Density,
) = with(density) {
) {
// placed center vertically and to the end edge horizontally
trailingPlaceable?.placeRelative(
width - trailingPlaceable.width,
Alignment.CenterVertically.align(trailingPlaceable.height, height)
Alignment.CenterVertically.align(trailingPlaceable.height, height),
)
// placed center vertically and after the leading icon horizontally if single line text field
// placed to the top with padding for multi line text field
// placed center vertically
textFieldPlaceable.placeRelative(
0,
Alignment.CenterVertically.align(textFieldPlaceable.height, height)
Alignment.CenterVertically.align(textFieldPlaceable.height, height),
)
// placed similar to the input text above
placeholderPlaceable?.let {
it.placeRelative(
0,
Alignment.CenterVertically.align(it.height, height)
)
}
placeholderPlaceable?.placeRelative(
0,
Alignment.CenterVertically.align(placeholderPlaceable.height, height),
)
}
private const val PLACEHOLDER_ID = "Placeholder"

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified
import androidx.compose.ui.unit.takeOrElse
import androidx.compose.ui.unit.toSize
@@ -49,14 +50,26 @@ fun Modifier.border(stroke: Stroke, shape: Shape): Modifier = when (stroke) {
width = stroke.width,
brush = stroke.brush,
shape = shape,
expand = stroke.expand
expand = stroke.expand,
)
}
fun Modifier.border(alignment: Stroke.Alignment, width: Dp, color: Color, shape: Shape = RectangleShape, expand: Dp = Dp.Unspecified) =
fun Modifier.border(
alignment: Stroke.Alignment,
width: Dp,
color: Color,
shape: Shape = RectangleShape,
expand: Dp = Dp.Unspecified,
) =
border(alignment, width, SolidColor(color), shape, expand)
fun Modifier.border(alignment: Stroke.Alignment, width: Dp, brush: Brush, shape: Shape = RectangleShape, expand: Dp = Dp.Unspecified): Modifier =
fun Modifier.border(
alignment: Stroke.Alignment,
width: Dp,
brush: Brush,
shape: Shape = RectangleShape,
expand: Dp = Dp.Unspecified,
): Modifier =
if (alignment == Stroke.Alignment.Inside && expand.isUnspecified) {
// The compose native border modifier(androidx.compose.foundation.border) draws the border inside the shape,
// so we can just use that for getting a more native experience when drawing inside borders
@@ -81,11 +94,13 @@ private fun Modifier.drawBorderWithAlignment(
val strokeWidthPx = max(
min(
if (width == Dp.Hairline) 1f else ceil(width.toPx()),
ceil(size.minDimension / 2)
ceil(size.minDimension / 2),
),
1f
1f,
)
val expandWidthPx = expand.takeOrElse { Dp.Hairline }.toPx()
val expandWidthPx = expand.takeOrElse { 0.dp }.toPx()
when (val outline = shape.createOutline(size, layoutDirection, this)) {
is Outline.Rectangle -> {
when (shape) {
@@ -95,19 +110,40 @@ private fun Modifier.drawBorderWithAlignment(
Outline.Rounded(RoundRect(outline.rect)),
brush,
strokeWidthPx,
expandWidthPx
expandWidthPx,
)
else -> drawRectBorder(borderCacheRef, alignment, outline, brush, strokeWidthPx, expandWidthPx)
else -> drawRectBorder(
borderCacheRef,
alignment,
outline,
brush,
strokeWidthPx,
expandWidthPx,
)
}
}
is Outline.Rounded -> drawRoundedBorder(borderCacheRef, alignment, outline, brush, strokeWidthPx, expandWidthPx)
is Outline.Rounded -> drawRoundedBorder(
borderCacheRef,
alignment,
outline,
brush,
strokeWidthPx,
expandWidthPx,
)
is Outline.Generic -> drawGenericBorder(borderCacheRef, alignment, outline, brush, strokeWidthPx, expandWidthPx)
is Outline.Generic -> drawGenericBorder(
borderCacheRef,
alignment,
outline,
brush,
strokeWidthPx,
expandWidthPx,
)
}
}
}
},
)
},
inspectorInfo = debugInspectorInfo {
@@ -122,7 +158,7 @@ private fun Modifier.drawBorderWithAlignment(
}
properties["shape"] = shape
properties["expand"] = expand
}
},
)
private class BorderCache(
@@ -151,7 +187,7 @@ private class BorderCache(
targetImageBitmap = ImageBitmap(
borderSize.width,
borderSize.height,
config = config
config = config,
).also {
imageBitmap = it
}
@@ -166,12 +202,12 @@ private class BorderCache(
this,
layoutDirection,
targetCanvas,
drawSize
drawSize,
) {
drawRect(
color = Color.Black,
size = drawSize,
blendMode = BlendMode.Clear
blendMode = BlendMode.Clear,
)
block()
}
@@ -219,26 +255,27 @@ private fun ContentDrawScope.drawRoundedBorder(
strokeWidthPx: Float,
expandWidthPx: Float,
) {
val rrect = when (alignment) {
val roundRect = when (alignment) {
Stroke.Alignment.Inside -> outline.roundRect.inflate(expandWidthPx - strokeWidthPx / 2f)
Stroke.Alignment.Center -> outline.roundRect.inflate(expandWidthPx)
Stroke.Alignment.Outside -> outline.roundRect.inflate(expandWidthPx + strokeWidthPx / 2f)
}
if (rrect.hasRightAngle()) {
if (roundRect.hasAtLeastOneNonRoundedCorner()) {
// Note: why do we need this? The Outline API can handle it just fine
val cache = borderCacheRef.obtain()
val borderPath = cache.obtainPath().apply {
reset()
fillType = PathFillType.EvenOdd
addRoundRect(rrect.deflate(strokeWidthPx / 2f))
addRoundRect(rrect.inflate(strokeWidthPx / 2f))
addRoundRect(roundRect.deflate(strokeWidthPx / 2f))
addRoundRect(roundRect.inflate(strokeWidthPx / 2f))
}
drawPath(borderPath, brush)
} else {
drawOutline(
outline = Outline.Rounded(rrect),
outline = Outline.Rounded(roundRect),
brush = brush,
style = DrawScopeStroke(strokeWidthPx)
style = DrawScopeStroke(strokeWidthPx),
)
}
}
@@ -276,10 +313,12 @@ private fun CacheDrawScope.drawGenericBorder(
inner -> {
return@onDrawWithContent
}
-inner -> {
// Samply draw the outline when abs(outer) and abs(inner) are the same
drawOutline(outline, brush, style = DrawScopeStroke(outer * 2f))
}
else -> {
val config: ImageBitmapConfig
val colorFilter: ColorFilter?
@@ -300,13 +339,13 @@ private fun CacheDrawScope.drawGenericBorder(
val cacheImageBitmap: ImageBitmap
val pathBoundsSize = IntSize(
ceil(pathBounds.width).toInt(),
ceil(pathBounds.height).toInt()
ceil(pathBounds.height).toInt(),
)
with(borderCache) {
cacheImageBitmap = drawBorderCache(
pathBoundsSize,
config
config,
) {
translate(-pathBounds.left, -pathBounds.top) {
if (inner < 0f && outer > 0f) {
@@ -317,7 +356,12 @@ private fun CacheDrawScope.drawGenericBorder(
drawPath(path = outline.path, brush = brush, style = DrawScopeStroke(outer * 2f))
if (inner > 0f) {
drawPath(path = outline.path, brush = brush, blendMode = BlendMode.Clear, style = DrawScopeStroke(inner * 2f))
drawPath(
path = outline.path,
brush = brush,
blendMode = BlendMode.Clear,
style = DrawScopeStroke(inner * 2f),
)
}
drawPath(path = outline.path, brush = brush, blendMode = BlendMode.Clear)
@@ -327,7 +371,12 @@ private fun CacheDrawScope.drawGenericBorder(
drawPath(path = outline.path, brush = brush, style = DrawScopeStroke(-inner * 2f))
if (outer < 0f) {
drawPath(path = outline.path, brush = brush, blendMode = BlendMode.Clear, style = DrawScopeStroke(-outer * 2f))
drawPath(
path = outline.path,
brush = brush,
blendMode = BlendMode.Clear,
style = DrawScopeStroke(-outer * 2f),
)
}
drawPath(path = outerMaskPath, brush = brush, blendMode = BlendMode.Clear)

View File

@@ -11,7 +11,7 @@ internal fun RoundRect.inflate(delta: Float) = RoundRect(
topLeftCornerRadius = CornerRadius(topLeftCornerRadius.x + delta, topLeftCornerRadius.y + delta),
topRightCornerRadius = CornerRadius(topRightCornerRadius.x + delta, topRightCornerRadius.y + delta),
bottomLeftCornerRadius = CornerRadius(bottomLeftCornerRadius.x + delta, bottomLeftCornerRadius.y + delta),
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x + delta, bottomRightCornerRadius.y + delta)
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x + delta, bottomRightCornerRadius.y + delta),
)
internal fun RoundRect.deflate(delta: Float) = RoundRect(
@@ -22,10 +22,10 @@ internal fun RoundRect.deflate(delta: Float) = RoundRect(
topLeftCornerRadius = CornerRadius(topLeftCornerRadius.x - delta, topLeftCornerRadius.y - delta),
topRightCornerRadius = CornerRadius(topRightCornerRadius.x - delta, topRightCornerRadius.y - delta),
bottomLeftCornerRadius = CornerRadius(bottomLeftCornerRadius.x - delta, bottomLeftCornerRadius.y - delta),
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x - delta, bottomRightCornerRadius.y - delta)
bottomRightCornerRadius = CornerRadius(bottomRightCornerRadius.x - delta, bottomRightCornerRadius.y - delta),
)
internal fun RoundRect.hasRightAngle() =
internal fun RoundRect.hasAtLeastOneNonRoundedCorner() =
topLeftCornerRadius.x == 0f && topLeftCornerRadius.y == 0f ||
topRightCornerRadius.x == 0f && topRightCornerRadius.y == 0f ||
bottomLeftCornerRadius.x == 0f && bottomLeftCornerRadius.y == 0f ||

View File

@@ -141,8 +141,8 @@ open class DefaultSelectableOnKeyEvent(
selectableState.addElementsToSelection(
listOf(
currentIndex,
prevIndex
)
prevIndex,
),
)
selectableState.lastKeyEventUsedMouse = true
}
@@ -170,8 +170,8 @@ open class DefaultSelectableOnKeyEvent(
selectableState.addElementsToSelection(
listOf(
currentIndex,
nextSelectableIndex
)
nextSelectableIndex,
),
)
selectableState.lastKeyEventUsedMouse = true
}

View File

@@ -68,7 +68,7 @@ fun SelectableLazyColumn(
interactionSource = interactionSource,
keyActions = keyActions.handleOnKeyEvent(rememberCoroutineScope()),
pointerHandlingScopedActions = pointerHandlingScopedActions,
content = content
content = content,
)
}
@@ -97,7 +97,7 @@ internal fun BaseSelectableLazyColumn(
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior
flingBehavior = flingBehavior,
) {
state.clearKeys()
SelectableLazyListScopeContainer(state, pointerHandlingScopedActions).apply(content)

View File

@@ -149,7 +149,7 @@ internal class SelectableLazyListScopeContainer(
Box(
Modifier
.then(if (selectable) Modifier.selectable(selectableKey, scope) else Modifier)
.then(if (focusable) Modifier.focusable(selectableKey as SelectableKey.Focusable, isFocused) else Modifier)
.then(if (focusable) Modifier.focusable(selectableKey as SelectableKey.Focusable, isFocused) else Modifier),
) {
content(SelectableLazyItemScope(isSelected, isFocused))
}
@@ -170,7 +170,7 @@ internal class SelectableLazyListScopeContainer(
} else {
SelectableKey.NotFocusable(
key(it),
selectable(it)
selectable(it),
)
}
}
@@ -187,11 +187,11 @@ internal class SelectableLazyListScopeContainer(
Box(
Modifier
.then(if (selectable(index)) Modifier.selectable(selectableKeys[index]) else Modifier)
.then(if (focusable(index)) Modifier.focusable(selectableKeys[index] as SelectableKey.Focusable, isFocused) else Modifier)
.then(if (focusable(index)) Modifier.focusable(selectableKeys[index] as SelectableKey.Focusable, isFocused) else Modifier),
) {
itemContent(SelectableLazyItemScope(isFocused, isSelected), index)
}
}
},
)
}

View File

@@ -29,6 +29,15 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.CommonStateBitMask
import org.jetbrains.jewel.CommonStateBitMask.Active
import org.jetbrains.jewel.CommonStateBitMask.Enabled
import org.jetbrains.jewel.CommonStateBitMask.Focused
import org.jetbrains.jewel.CommonStateBitMask.Hovered
import org.jetbrains.jewel.CommonStateBitMask.Pressed
import org.jetbrains.jewel.CommonStateBitMask.Selected
import org.jetbrains.jewel.InteractiveComponentState
import org.jetbrains.jewel.SelectableComponentState
import org.jetbrains.jewel.foundation.lazy.SelectableLazyColumn
import org.jetbrains.jewel.foundation.lazy.SelectableLazyItemScope
import org.jetbrains.jewel.foundation.utils.Log
@@ -99,7 +108,7 @@ fun <T> BasicLazyTree(
state = treeState.delegate,
keyActions = keyActions,
interactionSource = interactionSource,
pointerHandlingScopedActions = pointerEventScopedActions
pointerHandlingScopedActions = pointerEventScopedActions,
) {
items(
count = flattenedTree.size,
@@ -107,13 +116,13 @@ fun <T> BasicLazyTree(
val idPath = flattenedTree[it].idPath()
idPath
},
contentType = { flattenedTree[it].data }
contentType = { flattenedTree[it].data },
) { itemIndex ->
val element = flattenedTree[itemIndex]
val elementState = TreeElementState.of(
focused = isFocused,
selected = isSelected,
expanded = (element as? Tree.Element.Node)?.let { it.idPath() in treeState.openNodes } ?: false
expanded = (element as? Tree.Element.Node)?.let { it.idPath() in treeState.openNodes } ?: false,
)
val backgroundShape by remember { mutableStateOf(RoundedCornerShape(elementBackgroundCornerSize)) }
@@ -126,32 +135,32 @@ fun <T> BasicLazyTree(
elementBackgroundSelectedFocused,
elementBackgroundFocused,
elementBackgroundSelected,
backgroundShape
backgroundShape,
)
.padding(elementContentPadding)
.padding(start = (element.depth * indentSize.value).dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
indication = null,
) {
(pointerEventScopedActions as? DefaultTreeViewPointerEventAction)?.notifyItemClicked(
item = flattenedTree[itemIndex] as Tree.Element<T>,
scope = scope,
doubleClickTimeDelayMillis = platformDoubleClickDelay.inWholeMilliseconds,
onElementClick = onElementClick,
onElementDoubleClick = onElementDoubleClick
onElementDoubleClick = onElementDoubleClick,
)
}
},
) {
if (element is Tree.Element.Node) {
Box(
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
indication = null,
) {
treeState.toggleNode(element.idPath())
onElementDoubleClick(element as Tree.Element<T>)
}
},
) {
chevronContent(elementState)
}
@@ -177,54 +186,90 @@ private fun Modifier.elementBackground(
state.isSelected && !state.isFocused -> selected
else -> Color.Unspecified
},
shape = backgroundShape
shape = backgroundShape,
)
@Immutable
@JvmInline
value class TreeElementState(val state: ULong) {
value class TreeElementState(val state: ULong) : InteractiveComponentState, SelectableComponentState {
@Stable
val isFocused: Boolean
override val isActive: Boolean
get() = state and Active != 0UL
@Stable
override val isEnabled: Boolean
get() = state and Enabled != 0UL
@Stable
override val isFocused: Boolean
get() = state and Focused != 0UL
@Stable
val isSelected: Boolean
override val isPressed: Boolean
get() = state and Pressed != 0UL
@Stable
override val isHovered: Boolean
get() = state and Hovered != 0UL
@Stable
override val isSelected: Boolean
get() = state and Selected != 0UL
@Stable
val isExpanded: Boolean
get() = state and Expanded != 0UL
fun copy(
focused: Boolean = isFocused,
selected: Boolean = isSelected,
expanded: Boolean = isExpanded,
) = of(focused, selected, expanded)
override fun toString(): String =
"${javaClass.simpleName}(enabled=$isEnabled, focused=$isFocused, expanded=$isExpanded, " +
"pressed=$isPressed, hovered=$isHovered, active=$isActive, selected=$isSelected)"
override fun toString() =
"${javaClass.simpleName}(isFocused=$isFocused, isSelected=$isSelected, isExpanded=$isExpanded)"
fun copy(
enabled: Boolean = isEnabled,
focused: Boolean = isFocused,
expanded: Boolean = isExpanded,
pressed: Boolean = isPressed,
hovered: Boolean = isHovered,
active: Boolean = isActive,
selected: Boolean = isSelected,
) = of(
enabled = enabled,
focused = focused,
expanded = expanded,
pressed = pressed,
hovered = hovered,
active = active,
selected = selected,
)
companion object {
private val Focused = 1UL shl 0
private val Hovered = 1UL shl 1
private val Selected = 1UL shl 2
private val Expanded = 1UL shl 3
private const val EXPANDED_BIT_OFFSET = CommonStateBitMask.FIRST_AVAILABLE_OFFSET
private val Expanded = 1UL shl EXPANDED_BIT_OFFSET
fun of(
focused: Boolean,
selected: Boolean,
expanded: Boolean,
enabled: Boolean = true,
focused: Boolean = false,
expanded: Boolean = false,
hovered: Boolean = false,
pressed: Boolean = false,
active: Boolean = false,
selected: Boolean = false,
) = TreeElementState(
(if (focused) Focused else 0UL) or
(if (expanded) Expanded else 0UL) or
(if (enabled) Enabled else 0UL) or
(if (focused) Focused else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (selected) Selected else 0UL) or
(if (expanded) Expanded else 0UL)
(if (active) Active else 0UL),
)
}
}
private suspend fun flattenTree(
private fun flattenTree(
element: Tree.Element<*>,
openNodes: SnapshotStateList<Any>,
allNodes: SnapshotStateList<Any>,
@@ -241,7 +286,7 @@ private suspend fun flattenTree(
openNodes.removeAll(
buildList {
getAllSubNodes(element)
}
},
)
}
}

View File

@@ -49,7 +49,7 @@ class TreeBuilder<T> : TreeGeneratorScope<T> {
parent = null,
previous = previous,
next = null,
id = elementBuilder.id
id = elementBuilder.id,
)
is Element.Node -> Tree.Element.Node(
@@ -60,7 +60,7 @@ class TreeBuilder<T> : TreeGeneratorScope<T> {
childrenGenerator = { parent -> generateElements(parent, elementBuilder) },
previous = previous,
next = null,
id = elementBuilder.id
id = elementBuilder.id,
)
}
elements.add(current)
@@ -87,7 +87,7 @@ private fun <T> generateElements(
parent = parent,
previous = previous,
next = null,
id = elementBuilder.id
id = elementBuilder.id,
)
is TreeBuilder.Element.Node -> Tree.Element.Node(
@@ -98,7 +98,7 @@ private fun <T> generateElements(
childrenGenerator = { generateElements(it, elementBuilder) },
previous = previous,
next = null,
id = elementBuilder.id
id = elementBuilder.id,
)
}
previous.next = current
@@ -116,6 +116,7 @@ private fun <T> evaluatePrevious(element: Tree.Element<T>): Tree.Element<T> = wh
}
interface TreeGeneratorScope<T> {
fun addNode(
data: T,
id: Any = data.hashCode(),

View File

@@ -56,9 +56,9 @@ open class DefaultTreeViewOnKeyEvent(
treeState.addElementsToSelection(
listOf(
currentIndex,
prevIndex
prevIndex,
),
null
null,
)
treeState.lastKeyEventUsedMouse = true
}
@@ -88,9 +88,9 @@ open class DefaultTreeViewOnKeyEvent(
treeState.addElementsToSelection(
listOf(
currentIndex,
nextFlattenIndex
nextFlattenIndex,
),
null
null,
)
treeState.lastKeyEventUsedMouse = true
}

View File

@@ -13,8 +13,8 @@ fun rememberTreeState(selectionMode: SelectionMode = SelectionMode.Single) = rem
TreeState(
SelectableLazyListState(
LazyListState(),
selectionMode
)
selectionMode,
),
)
}

View File

@@ -37,8 +37,8 @@ interface ButtonColors {
focused = backgroundFocused,
pressed = backgroundPressed,
hovered = backgroundHovered,
active = background
)
active = background,
),
)
val content: Color
@@ -55,15 +55,15 @@ interface ButtonColors {
focused = contentFocused,
pressed = contentPressed,
hovered = contentHovered,
active = content
)
active = content,
),
)
val border: Color
val borderDisabled: Color
val borderFocused: Color
val borderPressed: Color
val borderHovered: Color
val border: Brush
val borderDisabled: Brush
val borderFocused: Brush
val borderPressed: Brush
val borderHovered: Brush
@Composable
fun borderFor(state: ButtonState) = rememberUpdatedState(
@@ -73,8 +73,8 @@ interface ButtonColors {
focused = borderFocused,
pressed = borderPressed,
hovered = borderHovered,
active = border
)
active = border,
),
)
}

View File

@@ -33,7 +33,7 @@ interface CheckboxColors {
!state.isEnabled -> checkboxBackgroundDisabled
state.toggleableState == ToggleableState.On -> checkboxBackgroundSelected
else -> checkboxBackground
}
},
)
val content: Color
@@ -46,20 +46,7 @@ interface CheckboxColors {
!state.isEnabled -> contentDisabled
state.toggleableState == ToggleableState.On -> contentSelected
else -> content
}
)
val checkboxBorder: Color
val checkboxBorderDisabled: Color
val checkboxBorderSelected: Color
@Composable
fun borderFor(state: CheckboxState) = rememberUpdatedState(
when {
!state.isEnabled -> checkboxBorderDisabled
state.toggleableState == ToggleableState.On -> checkboxBorderSelected
else -> checkboxBorder
}
},
)
}

View File

@@ -10,8 +10,8 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import org.jetbrains.jewel.ChipState
import org.jetbrains.jewel.IntelliJTheme
@Stable
interface ChipStyle {
@@ -28,17 +28,31 @@ interface ChipColors {
val backgroundFocused: Brush
val backgroundPressed: Brush
val backgroundHovered: Brush
val backgroundSelected: Brush
val backgroundSelectedDisabled: Brush
val backgroundSelectedPressed: Brush
val backgroundSelectedFocused: Brush
val backgroundSelectedHovered: Brush
@Composable
fun backgroundFor(state: ChipState) = rememberUpdatedState(
state.chooseValue(
normal = background,
disabled = backgroundDisabled,
focused = backgroundFocused,
pressed = backgroundPressed,
hovered = backgroundHovered,
active = background
)
if (state.isSelected) {
when {
!state.isEnabled -> backgroundSelectedDisabled
state.isPressed -> backgroundSelectedPressed
state.isFocused -> backgroundSelectedFocused
state.isHovered -> backgroundSelectedHovered
else -> backgroundSelected
}
} else {
when {
!state.isEnabled -> backgroundDisabled
state.isPressed -> backgroundPressed
state.isFocused -> backgroundFocused
state.isHovered -> backgroundHovered
else -> background
}
},
)
val content: Color
@@ -46,17 +60,31 @@ interface ChipColors {
val contentFocused: Color
val contentPressed: Color
val contentHovered: Color
val contentSelected: Color
val contentSelectedDisabled: Color
val contentSelectedPressed: Color
val contentSelectedFocused: Color
val contentSelectedHovered: Color
@Composable
fun contentFor(state: ChipState) = rememberUpdatedState(
state.chooseValue(
normal = content,
disabled = contentDisabled,
focused = contentFocused,
pressed = contentPressed,
hovered = contentHovered,
active = content
)
if (state.isSelected) {
when {
!state.isEnabled -> contentSelectedDisabled
state.isPressed -> contentSelectedPressed
state.isFocused -> contentSelectedFocused
state.isHovered -> contentSelectedHovered
else -> contentSelected
}
} else {
when {
!state.isEnabled -> contentDisabled
state.isPressed -> contentPressed
state.isFocused -> contentFocused
state.isHovered -> contentHovered
else -> content
}
},
)
val border: Color
@@ -64,27 +92,41 @@ interface ChipColors {
val borderFocused: Color
val borderPressed: Color
val borderHovered: Color
val borderSelected: Color
val borderSelectedDisabled: Color
val borderSelectedPressed: Color
val borderSelectedFocused: Color
val borderSelectedHovered: Color
@Composable
fun borderFor(state: ChipState) = rememberUpdatedState(
state.chooseValue(
normal = border,
disabled = borderDisabled,
focused = borderFocused,
pressed = borderPressed,
hovered = borderHovered,
active = border
)
if (state.isSelected) {
when {
!state.isEnabled -> borderSelectedDisabled
state.isPressed && !IntelliJTheme.isSwingCompatMode -> borderSelectedPressed
state.isFocused -> borderSelectedFocused
state.isHovered && !IntelliJTheme.isSwingCompatMode -> borderSelectedHovered
else -> borderSelected
}
} else {
when {
!state.isEnabled -> borderDisabled
state.isPressed && !IntelliJTheme.isSwingCompatMode -> borderPressed
state.isFocused -> borderFocused
state.isHovered && !IntelliJTheme.isSwingCompatMode -> borderHovered
else -> border
}
},
)
}
@Stable
interface ChipMetrics {
val minSize: DpSize
val cornerSize: CornerSize
val padding: PaddingValues
val borderWidth: Dp
val borderWidthSelected: Dp
}
val LocalChipStyle = staticCompositionLocalOf<ChipStyle> {

View File

@@ -31,21 +31,17 @@ interface DropdownColors {
val backgroundFocused: Color
val backgroundPressed: Color
val backgroundHovered: Color
val backgroundWarning: Color
val backgroundError: Color
@Composable
fun backgroundFor(state: DropdownState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = background,
disabled = backgroundDisabled,
focused = backgroundFocused,
pressed = backgroundPressed,
hovered = backgroundHovered,
warning = backgroundWarning,
error = backgroundError,
active = background
)
active = background,
),
)
val content: Color
@@ -53,21 +49,17 @@ interface DropdownColors {
val contentFocused: Color
val contentPressed: Color
val contentHovered: Color
val contentWarning: Color
val contentError: Color
@Composable
fun contentFor(state: DropdownState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = content,
disabled = contentDisabled,
focused = contentFocused,
pressed = contentPressed,
hovered = contentHovered,
warning = contentWarning,
error = contentError,
active = content
)
active = content,
),
)
val border: Color
@@ -75,21 +67,17 @@ interface DropdownColors {
val borderFocused: Color
val borderPressed: Color
val borderHovered: Color
val borderWarning: Color
val borderError: Color
@Composable
fun borderFor(state: DropdownState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = border,
disabled = borderDisabled,
focused = borderFocused,
pressed = borderPressed,
hovered = borderHovered,
warning = borderWarning,
error = borderError,
active = border
)
active = border,
),
)
val iconTint: Color
@@ -97,27 +85,24 @@ interface DropdownColors {
val iconTintFocused: Color
val iconTintPressed: Color
val iconTintHovered: Color
val iconTintWarning: Color
val iconTintError: Color
@Composable
fun iconTintFor(state: DropdownState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = iconTint,
disabled = iconTintDisabled,
focused = iconTintFocused,
pressed = iconTintPressed,
hovered = iconTintHovered,
warning = iconTintWarning,
error = iconTintError,
active = iconTint
)
active = iconTint,
),
)
}
@Stable
interface DropdownMetrics {
val arrowMinSize: DpSize
val minSize: DpSize
val cornerSize: CornerSize
val contentPadding: PaddingValues

View File

@@ -20,6 +20,7 @@ interface HorizontalProgressBarColors {
val track: Color
val progress: Color
val indeterminateBase: Color
val indeterminateHighlight: Color
}

View File

@@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
@@ -29,21 +28,17 @@ interface InputFieldColors {
val backgroundFocused: Color
val backgroundPressed: Color
val backgroundHovered: Color
val backgroundWarning: Color
val backgroundError: Color
@Composable
fun backgroundFor(state: InputFieldState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = background,
disabled = backgroundDisabled,
focused = backgroundFocused,
pressed = backgroundPressed,
hovered = backgroundHovered,
warning = backgroundWarning,
error = backgroundError,
active = background
)
active = background,
),
)
val content: Color
@@ -51,21 +46,17 @@ interface InputFieldColors {
val contentFocused: Color
val contentPressed: Color
val contentHovered: Color
val contentWarning: Color
val contentError: Color
@Composable
fun contentFor(state: InputFieldState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = content,
disabled = contentDisabled,
focused = contentFocused,
pressed = contentPressed,
hovered = contentHovered,
warning = contentWarning,
error = contentError,
active = content
)
active = content,
),
)
val border: Color
@@ -73,43 +64,35 @@ interface InputFieldColors {
val borderFocused: Color
val borderPressed: Color
val borderHovered: Color
val borderWarning: Color
val borderError: Color
@Composable
fun borderFor(state: InputFieldState) = rememberUpdatedState(
state.chooseValueWithOutline(
state.chooseValue(
normal = border,
disabled = borderDisabled,
focused = borderFocused,
pressed = borderPressed,
hovered = borderHovered,
warning = borderWarning,
error = borderError,
active = border
)
active = border,
),
)
val cursor: Brush
val cursorDisabled: Brush
val cursorFocused: Brush
val cursorPressed: Brush
val cursorHovered: Brush
val cursorWarning: Brush
val cursorError: Brush
val caret: Color
val caretDisabled: Color
val caretFocused: Color
val caretPressed: Color
val caretHovered: Color
@Composable
fun cursorFor(state: InputFieldState) = rememberUpdatedState(
state.chooseValueWithOutline(
normal = cursor,
disabled = cursorDisabled,
focused = cursorFocused,
pressed = cursorPressed,
hovered = cursorHovered,
warning = cursorWarning,
error = cursorError,
active = cursor
)
fun caretFor(state: InputFieldState) = rememberUpdatedState(
state.chooseValue(
normal = caret,
disabled = caretDisabled,
focused = caretFocused,
pressed = caretPressed,
hovered = caretHovered,
active = caret,
),
)
}

View File

@@ -8,10 +8,8 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.ResourceLoader
import androidx.compose.ui.unit.Dp
import org.jetbrains.jewel.foundation.tree.TreeElementState
import org.jetbrains.jewel.painterResource
@Stable
interface LazyTreeStyle {
@@ -33,11 +31,6 @@ interface LazyTreeColors {
val contentSelected: Color
val contentSelectedFocused: Color
val chevronTint: Color
val chevronTintSelected: Color
val chevronTintFocused: Color
val chevronTintSelectedFocused: Color
@Composable
fun contentFor(state: TreeElementState) = rememberUpdatedState(
when {
@@ -45,17 +38,7 @@ interface LazyTreeColors {
state.isFocused -> contentFocused
state.isSelected -> contentSelected
else -> content
}
)
@Composable
fun chevronTintFor(state: TreeElementState) = rememberUpdatedState(
when {
state.isSelected && state.isFocused -> chevronTintSelectedFocused
state.isFocused -> chevronTintFocused
state.isSelected -> chevronTintSelected
else -> chevronTint
}
},
)
}
@@ -73,10 +56,12 @@ interface LazyTreeMetrics {
@Immutable
interface LazyTreeIcons {
val nodeChevron: String
val nodeChevronCollapsed: StatefulPainterProvider<TreeElementState>
val nodeChevronExpanded: StatefulPainterProvider<TreeElementState>
@Composable
fun nodeChevronPainter(resourceLoader: ResourceLoader) = painterResource(nodeChevron, resourceLoader)
fun nodeChevron(isExpanded: Boolean) =
if (isExpanded) nodeChevronExpanded else nodeChevronCollapsed
}
val LocalLazyTreeStyle = staticCompositionLocalOf<LazyTreeStyle> {

View File

@@ -39,12 +39,9 @@ interface LinkColors {
pressed = contentPressed,
hovered = contentHovered,
visited = contentVisited,
active = content
)
active = content,
),
)
val iconTint: Color
val iconTintDisabled: Color
}
@Immutable
@@ -81,8 +78,8 @@ interface LinkTextStyles {
pressed = pressed,
hovered = hovered,
visited = visited,
active = normal
)
active = normal,
),
)
}

View File

@@ -7,7 +7,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
@@ -24,7 +23,7 @@ interface MenuStyle {
@Immutable
interface MenuColors {
val background: Brush
val background: Color
val border: Color
val shadow: Color
val itemColors: MenuItemColors
@@ -34,8 +33,7 @@ interface MenuColors {
interface MenuMetrics {
val cornerSize: CornerSize
val margin: PaddingValues
val padding: PaddingValues
val menuMargin: PaddingValues
val contentPadding: PaddingValues
val offset: DpOffset
val shadowSize: Dp
@@ -47,17 +45,17 @@ interface MenuMetrics {
@Stable
interface MenuItemMetrics {
val cornerSize: CornerSize
val padding: PaddingValues
val selectionCornerSize: CornerSize
val outerPadding: PaddingValues
val contentPadding: PaddingValues
val separatorPadding: PaddingValues
val separatorThickness: Dp
}
@Stable
interface SubmenuMetrics {
val offset: DpOffset
val itemPadding: PaddingValues
}
@Immutable
@@ -77,8 +75,8 @@ interface MenuItemColors {
active = background,
focused = backgroundFocused,
pressed = backgroundPressed,
hovered = backgroundHovered
)
hovered = backgroundHovered,
),
)
val content: Color
@@ -95,8 +93,8 @@ interface MenuItemColors {
focused = contentFocused,
pressed = contentPressed,
hovered = contentHovered,
active = content
)
active = content,
),
)
val iconTint: Color
@@ -113,8 +111,8 @@ interface MenuItemColors {
focused = iconTintFocused,
pressed = iconTintPressed,
hovered = iconTintHovered,
active = iconTint
)
active = iconTint,
),
)
val separator: Color
@@ -122,7 +120,8 @@ interface MenuItemColors {
@Immutable
interface MenuIcons {
val submenuChevron: String
val submenuChevron: StatefulPainterProvider<MenuItemState>
}
val LocalMenuStyle = staticCompositionLocalOf<MenuStyle> {

View File

@@ -36,65 +36,7 @@ interface RadioButtonColors {
state.isSelected -> contentSelected
state.isHovered -> contentHovered
else -> content
}
)
val buttonColors: RadioButtonButtonColors
}
// TODO these should be used to tint the SVGs
@Immutable
interface RadioButtonButtonColors {
val fill: Color
val fillHovered: Color
val fillDisabled: Color
val fillSelected: Color
val fillSelectedHovered: Color
val fillSelectedDisabled: Color
@Composable
fun fillFor(state: RadioButtonState) = rememberUpdatedState(
when {
!state.isEnabled && state.isSelected -> fillSelectedDisabled
!state.isEnabled -> fillDisabled
state.isSelected && state.isHovered -> fillSelectedHovered
state.isSelected -> fillSelected
state.isHovered -> fillHovered
else -> fill
}
)
val border: Color
val borderHovered: Color
val borderDisabled: Color
val borderSelected: Color
val borderSelectedHovered: Color
val borderSelectedDisabled: Color
@Composable
fun borderFor(state: RadioButtonState) = rememberUpdatedState(
when {
!state.isEnabled && state.isSelected -> borderSelectedDisabled
!state.isEnabled -> borderDisabled
state.isSelected && state.isHovered -> borderSelectedHovered
state.isSelected -> borderSelected
state.isHovered -> borderHovered
else -> border
}
)
val markSelected: Color
val markSelectedHovered: Color
val markSelectedDisabled: Color
@Composable
fun markFor(state: RadioButtonState) = rememberUpdatedState(
when {
!state.isSelected -> Color.Unspecified
!state.isEnabled -> markSelectedDisabled
state.isHovered -> markSelectedHovered
else -> markSelected
}
},
)
}

View File

@@ -15,7 +15,7 @@ import org.jetbrains.jewel.SvgLoader
import org.jetbrains.jewel.painterResource
@Immutable
class ResourcePainterProvider<T : InteractiveComponentState>(
open class ResourcePainterProvider<T : InteractiveComponentState>(
private val basePath: String,
private val svgLoader: SvgLoader,
private val prefixTokensProvider: (state: T) -> String = { "" },
@@ -26,9 +26,9 @@ class ResourcePainterProvider<T : InteractiveComponentState>(
override fun getPainter(state: T, resourceLoader: ResourceLoader): State<Painter> {
val isSvg = basePath.endsWith(".svg", ignoreCase = true)
val painter = if (isSvg) {
svgLoader.loadSvgResource(basePath, resourceLoader) { patchPath(state, basePath) }
svgLoader.loadSvgResource(basePath, resourceLoader) { patchPath(state, basePath, resourceLoader) }
} else {
val patchedPath = patchPath(state, basePath)
val patchedPath = patchPath(state, basePath, resourceLoader)
painterResource(patchedPath, resourceLoader)
}
@@ -36,7 +36,7 @@ class ResourcePainterProvider<T : InteractiveComponentState>(
}
@Composable
private fun patchPath(state: T, basePath: String): String = buildString {
protected open fun patchPath(state: T, basePath: String, resourceLoader: ResourceLoader): String = buildString {
append(basePath.substringBeforeLast('/', ""))
append('/')
append(basePath.substringBeforeLast('.').substringAfterLast('/'))

View File

@@ -69,9 +69,9 @@ interface TabColors {
focused = contentFocused,
pressed = contentPressed,
hovered = contentHovered,
active = content
active = content,
)
}
},
)
@Composable
@@ -84,7 +84,7 @@ interface TabColors {
state.isActive -> background
state.isSelected -> backgroundSelected
else -> background
}
},
)
@Composable
@@ -97,9 +97,9 @@ interface TabColors {
focused = underlineFocused,
pressed = underlinePressed,
hovered = underlineHovered,
active = underline
active = underline,
)
}
},
)
}
@@ -123,9 +123,9 @@ interface TabContentAlpha {
focused = iconFocused,
pressed = iconPressed,
hovered = iconHovered,
active = iconNormal
active = iconNormal,
)
}
},
)
val labelNormal: Float
@@ -145,9 +145,9 @@ interface TabContentAlpha {
focused = labelFocused,
pressed = labelPressed,
hovered = labelHovered,
active = labelNormal
active = labelNormal,
)
}
},
)
}

View File

@@ -1,33 +1,21 @@
package org.jetbrains.jewel.styling
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import org.jetbrains.jewel.InputFieldState
@Stable
interface TextAreaStyle : InputFieldStyle {
override val colors: TextAreaColors
override val metrics: InputFieldMetrics
val hintTextStyle: TextStyle
}
@Immutable
interface TextAreaColors : InputFieldColors {
val placeholder: Color
val hintContent: Color
val hintContentDisabled: Color
@Composable
fun hintContentFor(state: InputFieldState) = rememberUpdatedState(
if (state.isEnabled) hintContent else hintContentDisabled
)
}
val LocalTextAreaStyle = staticCompositionLocalOf<TextAreaStyle> {

View File

@@ -55,7 +55,7 @@ object PaletteMapperFactory {
"Checkbox.Focus.Thin.Selected" to "#ACCFF7",
"Checkbox.Focus.Thin.Selected.Dark" to "#466D94",
"Tree.iconColor" to "#808080",
"Tree.iconColor.Dark" to "#AFB1B3"
"Tree.iconColor.Dark" to "#AFB1B3",
)
fun create(

View File

@@ -104,7 +104,9 @@ style:
includeLineWrapping: false
ForbiddenComment:
active: true
values: [ 'STOPSHIP' ]
comments:
- value: 'STOPSHIP'
reason: 'Forbidden STOPSHIP marker in comment, please address before shipping'
allowedPatterns: ''
LoopWithTooManyJumpStatements:
active: true

View File

@@ -1,23 +1,20 @@
[versions]
composeDesktop = "1.5.0-dev1136"
coroutines = "1.6.4"
detekt = "1.22.0"
composeDesktop = "1.5.0-rc02"
coroutines = "1.7.3"
detekt = "1.23.1"
idea = "232.8660.185"
ideaGradlePlugin = "1.13.0"
ideaGradlePlugin = "1.15.0"
javaSarif = "2.0"
kotlinSarif = "0.4.0"
jna = "5.10.0"
kotlin = "1.8.20"
kotlin = "1.8.21"
dokka = "1.8.20"
kotlinterGradlePlugin = "3.12.0"
kotlinterGradlePlugin = "3.16.0"
kotlinxSerialization = "1.5.1"
kotlinpoet = "1.14.2"
[libraries]
compose-components-splitpane = { module = "org.jetbrains.compose.components:components-splitpane", version.ref = "composeDesktop" }
javaSarif = { module = "com.contrastsecurity:java-sarif", version.ref = "javaSarif" }
kotlinSarif = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "kotlinSarif" }
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" }
ij-platform-ide-core = { module = "com.jetbrains.intellij.platform:ide-core", version.ref = "idea" }

View File

@@ -2,52 +2,42 @@
package org.jetbrains.jewel.bridge
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
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.LafManager
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.util.messages.MessageBus
import com.intellij.util.messages.SimpleMessageBusConnection
import com.intellij.util.messages.Topic
import com.intellij.util.ui.DirProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
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
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
internal val IntelliJApplication: Application
get() = ApplicationManager.getApplication()
internal val Application.intellijThemeFlow
get() = lookAndFeelFlow.map { ObtainIntelliJTheme() }
internal val Application.lookAndFeelFlow: Flow<LafManager>
get() = messageBusFlow(LafManagerListener.TOPIC, { LafManager.getInstance()!! }) {
LafManagerListener { trySend(it) }
private val Application.lookAndFeelFlow: Flow<Unit>
get() = messageBus.flow(LafManagerListener.TOPIC) {
LafManagerListener { trySend(Unit) }
}
internal fun <L : Any, K> Application.messageBusFlow(
internal fun Application.lookAndFeelChangedFlow(
scope: CoroutineScope,
sharingStarted: SharingStarted = SharingStarted.Eagerly,
): Flow<Unit> =
lookAndFeelFlow
.onStart { emit(Unit) }
.shareIn(scope, sharingStarted, replay = 1)
internal fun <L : Any, K> MessageBus.flow(
topic: Topic<L>,
initialValue: (suspend () -> K)? = null,
@BuilderInference listener: ProducerScope<K>.() -> L
listener: ProducerScope<K>.() -> L,
): Flow<K> = callbackFlow {
initialValue?.let { send(it()) }
val connection: SimpleMessageBusConnection = messageBus.simpleConnect()
val connection: SimpleMessageBusConnection = simpleConnect()
connection.subscribe(topic, listener())
awaitClose { connection.disconnect() }
}

View File

@@ -0,0 +1,136 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.BorderColors
import org.jetbrains.jewel.GlobalColors
import org.jetbrains.jewel.OutlineColors
@Immutable
internal class BridgeGlobalColors(
override val borders: BorderColors,
override val outlines: OutlineColors,
@SwingLafKey("*.infoForeground") override val infoContent: Color,
) : GlobalColors {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BridgeGlobalColors
if (borders != other.borders) return false
if (outlines != other.outlines) return false
if (infoContent != other.infoContent) return false
return true
}
override fun hashCode(): Int {
var result = borders.hashCode()
result = 31 * result + outlines.hashCode()
result = 31 * result + infoContent.hashCode()
return result
}
override fun toString(): String =
"BridgeGlobalColors(borders=$borders, outlines=$outlines, infoContent=$infoContent)"
companion object {
fun readFromLaF() = BridgeGlobalColors(
borders = BridgeBorderColors.readFromLaF(),
outlines = BridgeOutlineColors.readFromLaF(),
infoContent = retrieveColorOrUnspecified("*.infoForeground"),
)
}
}
@Immutable
internal class BridgeBorderColors(
@SwingLafKey("Component.borderColor") override val normal: Color,
@SwingLafKey("Component.focusedBorderColor") override val focused: Color,
@SwingLafKey("*.disabledBorderColor") override val disabled: Color,
) : BorderColors {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BridgeBorderColors
if (normal != other.normal) return false
if (focused != other.focused) return false
if (disabled != other.disabled) return false
return true
}
override fun hashCode(): Int {
var result = normal.hashCode()
result = 31 * result + focused.hashCode()
result = 31 * result + disabled.hashCode()
return result
}
override fun toString(): String =
"BridgeBorderColors(normal=$normal, focused=$focused, disabled=$disabled)"
companion object {
fun readFromLaF() = BridgeBorderColors(
normal = retrieveColorOrUnspecified("Component.borderColor"),
focused = retrieveColorOrUnspecified("Component.focusedBorderColor"),
disabled = retrieveColorOrUnspecified("*.disabledBorderColor"),
)
}
}
@Immutable
internal class BridgeOutlineColors(
@SwingLafKey("*.focusColor") override val focused: Color,
@SwingLafKey("Component.warningFocusColor") override val focusedWarning: Color,
@SwingLafKey("Component.errorFocusColor") override val focusedError: Color,
@SwingLafKey("Component.inactiveWarningFocusColor") override val warning: Color,
@SwingLafKey("Component.inactiveErrorFocusColor") override val error: Color,
) : OutlineColors {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BridgeOutlineColors
if (focused != other.focused) return false
if (focusedWarning != other.focusedWarning) return false
if (focusedError != other.focusedError) return false
if (warning != other.warning) return false
if (error != other.error) return false
return true
}
override fun hashCode(): Int {
var result = focused.hashCode()
result = 31 * result + focusedWarning.hashCode()
result = 31 * result + focusedError.hashCode()
result = 31 * result + warning.hashCode()
result = 31 * result + error.hashCode()
return result
}
override fun toString(): String =
"BridgeOutlineColors(focused=$focused, focusedWarning=$focusedWarning, focusedError=$focusedError, " +
"warning=$warning, error=$error)"
companion object {
fun readFromLaF() = BridgeOutlineColors(
focused = retrieveColorOrUnspecified("*.focusColor"),
focusedWarning = retrieveColorOrUnspecified("Component.warningFocusColor"),
focusedError = retrieveColorOrUnspecified("Component.errorFocusColor"),
warning = retrieveColorOrUnspecified("Component.inactiveWarningFocusColor"),
error = retrieveColorOrUnspecified("Component.inactiveErrorFocusColor"),
)
}
}

View File

@@ -0,0 +1,35 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.Dp
import com.intellij.ide.ui.laf.darcula.DarculaUIUtil
import org.jetbrains.jewel.GlobalMetrics
@Immutable
internal class BridgeGlobalMetrics(
override val outlineWidth: Dp,
) : GlobalMetrics {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BridgeGlobalMetrics
if (outlineWidth != other.outlineWidth) return false
return true
}
override fun hashCode(): Int = outlineWidth.hashCode()
override fun toString(): String =
"BridgeGlobalMetrics(outlineWidth=$outlineWidth)"
companion object {
fun readFromLaF() = BridgeGlobalMetrics(
outlineWidth = DarculaUIUtil.BW.dp,
)
}
}

View File

@@ -0,0 +1,38 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Immutable
import org.jetbrains.jewel.IntelliJThemeIconData
@Immutable
internal class BridgeIconData(
override val iconOverrides: Map<String, String>,
override val colorPalette: Map<String, String>,
) : IntelliJThemeIconData {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BridgeIconData
if (iconOverrides != other.iconOverrides) return false
if (colorPalette != other.colorPalette) return false
return true
}
override fun hashCode(): Int {
var result = iconOverrides.hashCode()
result = 31 * result + colorPalette.hashCode()
return result
}
override fun toString(): String =
"BridgeIconData(iconOverrides=$iconOverrides, colorPalette=$colorPalette)"
companion object {
// TODO retrieve icon data from Swing
fun readFromLaF() = BridgeIconData(emptyMap(), emptyMap())
}
}

View File

@@ -1,115 +0,0 @@
package org.jetbrains.jewel.bridge
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
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.openapi.diagnostic.Logger
import com.intellij.ui.NewUI
import com.intellij.util.ui.DirProvider
import org.jetbrains.jewel.IntelliJTheme
import org.jetbrains.skiko.toSkikoTypeface
import javax.swing.UIManager
private val logger = Logger.getInstance("JewelBridge")
private val dirProvider = DirProvider()
internal fun ObtainIntelliJTheme(): IntelliJTheme {
val isIntUi = NewUI.isEnabled()
if (!isIntUi) {
// TODO return Darcula/IntelliJ Light theme instead
logger.warn("Darcula LaF (aka \"old UI\" are not supported yet, falling back to Int UI")
}
return bridgeIntUi()
}
fun java.awt.Color.toComposeColor() = Color(
red = red,
green = green,
blue = blue,
alpha = alpha
)
internal fun retrieveColorOrNull(key: String) =
UIManager.getColor(key)?.toComposeColor()
internal fun retrieveColorOrUnspecified(key: String): Color {
val color = retrieveColorOrNull(key)
if (color == null) {
logger.warn("Color with key \"$key\" not found, fallback to 'Color.Unspecified'")
}
return color ?: Color.Unspecified
}
internal fun retrieveColorsOrUnspecified(vararg keys: String) = keys.map { retrieveColorOrUnspecified(it) }
// Based on LafIconLookup#findIcon
// TODO inject additional logic from ImageLoader#addFileNameVariant to support loading the right icon
// variants ([_dark], [@2x], [_stroke])
internal fun lookupIJSvgIcon(
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 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) }
internal suspend fun retrieveFont(
key: String,
color: Color = Color.Unspecified,
lineHeight: TextUnit = TextUnit.Unspecified
): TextStyle {
val font = UIManager.getFont(key) ?: error("Font with key \"$key\" not found, fallback to 'Typeface.makeDefault()'")
return with(font) {
val typeface = toSkikoTypeface() ?: org.jetbrains.skia.Typeface.makeDefault().also {
logger.warn("Unable to convert font ${font.fontName} into a Skiko typeface, fallback to 'Typeface.makeDefault()'")
}
TextStyle(
color = color,
fontSize = size.sp,
fontWeight = FontWeight.Normal,
fontFamily = FontFamily(Typeface(typeface)),
// todo textDecoration might be defined in the awt theme
lineHeight = lineHeight
)
}
}

View File

@@ -0,0 +1,95 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.intellij.openapi.diagnostic.Logger
import org.jetbrains.jewel.themes.intui.core.IntUiThemeColorPalette
private val logger = Logger.getInstance("BridgeThemeColorPalette")
@Immutable
class BridgeThemeColorPalette(
private val grey: List<Color>,
private val blue: List<Color>,
private val green: List<Color>,
private val red: List<Color>,
private val yellow: List<Color>,
private val orange: List<Color>,
private val purple: List<Color>,
private val teal: List<Color>,
) : IntUiThemeColorPalette {
override fun grey(): List<Color> = grey
override fun grey(index: Int): Color = grey[index - 1]
override fun blue(): List<Color> = blue
override fun blue(index: Int): Color = blue[index - 1]
override fun green(): List<Color> = green
override fun green(index: Int): Color = green[index - 1]
override fun red(): List<Color> = red
override fun red(index: Int): Color = red[index - 1]
override fun yellow(): List<Color> = yellow
override fun yellow(index: Int): Color = yellow[index - 1]
override fun orange(): List<Color> = orange
override fun orange(index: Int): Color = orange[index - 1]
override fun purple(): List<Color> = purple
override fun purple(index: Int): Color = purple[index - 1]
override fun teal(): List<Color> = teal
override fun teal(index: Int): Color = teal[index - 1]
companion object {
fun readFromLaF() = BridgeThemeColorPalette(
grey = readPaletteColors("Grey"),
blue = readPaletteColors("Blue"),
green = readPaletteColors("Green"),
red = readPaletteColors("Red"),
yellow = readPaletteColors("Yellow"),
orange = readPaletteColors("Orange"),
purple = readPaletteColors("Purple"),
teal = readPaletteColors("Teal"),
)
private fun readPaletteColors(colorName: String): List<Color> {
val defaults = uiDefaults
val allKeys = defaults.keys
val colorNameKeyPrefix = "ColorPalette.$colorName"
val colorNameKeyPrefixLength = colorNameKeyPrefix.length
val lastColorIndex = allKeys.asSequence()
.filterIsInstance(String::class.java)
.filter { it.startsWith(colorNameKeyPrefix) }
.mapNotNull {
val afterName = it.substring(colorNameKeyPrefixLength)
afterName.toIntOrNull()
}
.maxOrNull() ?: return emptyList()
return buildList {
for (i in 1..lastColorIndex) {
val value = defaults["$colorNameKeyPrefix$i"] as? java.awt.Color
if (value == null) {
logger.error("Unable to find color value for palette key '$colorNameKeyPrefix$i'")
continue
}
add(value.toComposeColor())
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
package org.jetbrains.jewel.bridge
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.TileMode
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.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.intellij.openapi.diagnostic.Logger
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBValue
import org.jetbrains.jewel.IntelliJThemeIconData
import org.jetbrains.jewel.InteractiveComponentState
import org.jetbrains.jewel.SvgLoader
import org.jetbrains.jewel.styling.StatefulPainterProvider
import org.jetbrains.skiko.DependsOnJBR
import org.jetbrains.skiko.awt.font.AwtFontManager
import org.jetbrains.skiko.toSkikoTypefaceOrNull
import javax.swing.UIManager
private val logger = Logger.getInstance("JewelBridge")
fun java.awt.Color.toComposeColor() = Color(
red = red,
green = green,
blue = blue,
alpha = alpha,
)
fun java.awt.Color?.toComposeColorOrUnspecified() = this?.toComposeColor() ?: Color.Unspecified
@Suppress("UnstableApiUsage")
internal fun retrieveColorOrNull(key: String) =
JBColor.namedColor(key)
.takeUnless { it.name == "NAMED_COLOR_FALLBACK_MARKER" }
?.toComposeColor()
internal fun retrieveColorOrUnspecified(key: String): Color {
val color = retrieveColorOrNull(key)
if (color == null) {
logger.warn("Color with key \"$key\" not found, fallback to 'Color.Unspecified'")
}
return color ?: Color.Unspecified
}
internal fun retrieveColorsOrUnspecified(vararg keys: String) = keys.map { retrieveColorOrUnspecified(it) }
internal fun List<Color>.createVerticalBrush(
startY: Float = 0.0f,
endY: Float = Float.POSITIVE_INFINITY,
tileMode: TileMode = TileMode.Clamp,
): Brush {
if (isEmpty()) return SolidColor(Color.Transparent)
if (size == 1) return SolidColor(first())
// Optimization: use a cheaper SolidColor if all colors are identical
if (all { it == first() }) SolidColor(first())
return Brush.verticalGradient(this, startY, endY, tileMode)
}
internal fun retrieveIntAsDp(key: String): Dp {
val rawValue = UIManager.get(key)
if (rawValue is Int) rawValue.dp
keyNotFound(key, "Int")
}
internal fun retrieveIntAsDpOrUnspecified(key: String) =
try {
retrieveIntAsDp(key)
} catch (ignored: JewelBridgeException) {
Dp.Unspecified
}
internal fun retrieveInsetsAsPaddingValues(key: String) =
UIManager.getInsets(key)
?.let { PaddingValues(it.left.dp, it.top.dp, it.right.dp, it.bottom.dp) }
?: keyNotFound(key, "Insets")
internal fun retrieveArcAsCornerSize(key: String) =
CornerSize(retrieveIntAsDp(key) / 2)
internal fun retrieveArcAsCornerSizeWithFallbacks(vararg keys: String): CornerSize {
for (key in keys) {
val rawValue = UIManager.get(key)
if (rawValue is Int) {
val cornerSize = rawValue.dp
// Swing uses arcs, which are a diameter length, but we need a radius
return CornerSize(cornerSize / 2)
}
}
keysNotFound(keys.toList(), "Int")
}
@OptIn(DependsOnJBR::class)
private val awtFontManager = AwtFontManager()
internal suspend fun retrieveTextStyle(fontKey: String, colorKey: String? = null): TextStyle {
val baseColor = colorKey?.let { retrieveColorOrUnspecified(colorKey) } ?: Color.Unspecified
return retrieveTextStyle(fontKey, color = baseColor)
}
@OptIn(DependsOnJBR::class)
internal suspend fun retrieveTextStyle(
key: String,
color: Color = Color.Unspecified,
lineHeight: TextUnit = TextUnit.Unspecified,
): TextStyle {
val font = UIManager.getFont(key) ?: keyNotFound(key, "Font")
return with(font) {
val typeface = toSkikoTypefaceOrNull(awtFontManager)
?: org.jetbrains.skia.Typeface.makeDefault()
.also {
logger.warn(
"Unable to convert font ${font.fontName} into a Skiko typeface, " +
"fallback to 'Typeface.makeDefault()'",
)
}
TextStyle(
color = color,
fontSize = size.sp,
fontWeight = FontWeight.Normal,
fontFamily = FontFamily(Typeface(typeface)),
// todo textDecoration might be defined in the awt theme
lineHeight = lineHeight,
)
}
}
internal val JBValue.dp
get() = unscaled.dp
internal fun TextStyle.derive(sizeDelta: Float, weight: FontWeight? = fontWeight, color: Color = toSpanStyle().color) =
copy(fontSize = fontSize - sizeDelta, fontWeight = weight, color = color)
internal operator fun TextUnit.minus(delta: Float) = plus(-delta)
internal operator fun TextUnit.plus(delta: Float) =
when {
isSp -> TextUnit(value + delta, type)
isEm -> TextUnit(value + delta, type)
else -> this
}
internal fun <T : InteractiveComponentState> retrieveIcon(
baseIconPath: String,
iconData: IntelliJThemeIconData,
svgLoader: SvgLoader,
prefixTokensProvider: (state: T) -> String = { "" },
suffixTokensProvider: (state: T) -> String = { "" },
): StatefulPainterProvider<T> = IntelliJResourcePainterProvider(
basePath = iconData.iconOverrides[baseIconPath] ?: baseIconPath,
svgLoader,
prefixTokensProvider,
suffixTokensProvider,
)

View File

@@ -0,0 +1,28 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Composable
import androidx.compose.ui.awt.ComposePanel
import com.intellij.openapi.wm.ToolWindow
fun ToolWindow.addComposePanel(
tabDisplayName: String,
isLockable: Boolean = true,
content: @Composable ComposePanel.() -> Unit,
) = ComposePanel {
content()
}.also { contentManager.addContent(contentManager.factory.createContent(it, tabDisplayName, isLockable)) }
internal fun ComposePanel(
height: Int = 800,
width: Int = 800,
y: Int = 0,
x: Int = 0,
content: @Composable ComposePanel.() -> Unit,
): ComposePanel {
val panel = ComposePanel()
panel.setBounds(x, y, width, height)
panel.setContent {
panel.content()
}
return panel
}

View File

@@ -1,43 +0,0 @@
package org.jetbrains.jewel.bridge
import androidx.compose.ui.text.TextStyle
import org.jetbrains.jewel.ButtonDefaults
import org.jetbrains.jewel.CheckboxDefaults
import org.jetbrains.jewel.ChipDefaults
import org.jetbrains.jewel.DropdownDefaults
import org.jetbrains.jewel.GroupHeaderDefaults
import org.jetbrains.jewel.IntelliJColors
import org.jetbrains.jewel.LabelledTextFieldDefaults
import org.jetbrains.jewel.LinkDefaults
import org.jetbrains.jewel.MenuDefaults
import org.jetbrains.jewel.ThemeColors
import org.jetbrains.jewel.ProgressBarDefaults
import org.jetbrains.jewel.RadioButtonDefaults
import org.jetbrains.jewel.ScrollThumbDefaults
import org.jetbrains.jewel.TextAreaDefaults
import org.jetbrains.jewel.TextFieldDefaults
import org.jetbrains.jewel.TreeDefaults
import org.jetbrains.jewel.themes.intui.core.IntUiColorPalette
import org.jetbrains.jewel.themes.intui.standalone.IntUiTheme
class IntUiBridgeTheme internal constructor(
override val isDark: Boolean,
palette: IntUiColorPalette,
override val colors: IntelliJColors,
val themeColors: ThemeColors,
override val buttonDefaults: ButtonDefaults,
override val checkboxDefaults: CheckboxDefaults,
override val groupHeaderDefaults: GroupHeaderDefaults,
override val linkDefaults: LinkDefaults,
override val textFieldDefaults: TextFieldDefaults,
override val labelledTextFieldDefaults: LabelledTextFieldDefaults,
override val textAreaDefaults: TextAreaDefaults,
override val radioButtonDefaults: RadioButtonDefaults,
override val dropdownDefaults: DropdownDefaults,
override val contextMenuDefaults: MenuDefaults,
override val defaultTextStyle: TextStyle,
override val treeDefaults: TreeDefaults,
override val chipDefaults: ChipDefaults,
override val scrollThumbDefaults: ScrollThumbDefaults,
override val progressBarDefaults: ProgressBarDefaults,
): IntUiTheme(palette)

View File

@@ -0,0 +1,59 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.ResourceLoader
import com.intellij.ide.ui.IconMapLoader
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import org.jetbrains.jewel.InteractiveComponentState
import org.jetbrains.jewel.JewelResourceLoader
import org.jetbrains.jewel.SvgLoader
import org.jetbrains.jewel.styling.ResourcePainterProvider
class IntelliJResourcePainterProvider<T : InteractiveComponentState>(
basePath: String,
svgLoader: SvgLoader,
prefixTokensProvider: (state: T) -> String = { "" },
suffixTokensProvider: (state: T) -> String = { "" },
) : ResourcePainterProvider<T>(basePath, svgLoader, prefixTokensProvider, suffixTokensProvider) {
private val logger = thisLogger()
private val mappingsByClassLoader
get() = service<IconMapLoader>().loadIconMapping()
@Composable
override fun patchPath(state: T, basePath: String, resourceLoader: ResourceLoader): String {
val patchedPath = super.patchPath(state, basePath, resourceLoader)
return mapPath(patchedPath, resourceLoader)
}
private fun mapPath(originalPath: String, resourceLoader: ResourceLoader): String {
logger.debug("Loading SVG from '$originalPath'")
val searchClasses = (resourceLoader as? JewelResourceLoader)?.searchClasses
if (searchClasses == null) {
logger.warn(
"Tried loading a resource but the provided ResourceLoader is now a JewelResourceLoader; " +
"this is probably a bug. Make sure you always use JewelResourceLoaders.",
)
return originalPath
}
val allMappings = mappingsByClassLoader
if (allMappings.isEmpty()) {
logger.info("No mapping info available yet, can't check for '$originalPath' mapping.")
return originalPath
}
val applicableMappings = searchClasses.mapNotNull { allMappings[it.classLoader] }
val mappedPath = applicableMappings.firstNotNullOfOrNull { it[originalPath.removePrefix("/")] }
if (mappedPath == null) {
logger.debug("Icon '$originalPath' has no mapping defined.")
return originalPath
}
logger.debug("Icon '$originalPath' is mapped to '$mappedPath'.")
return mappedPath
}
}

View File

@@ -0,0 +1,21 @@
package org.jetbrains.jewel.bridge
sealed class JewelBridgeException(override val message: String?) : RuntimeException(message) {
class KeyNotFoundException(key: String, type: String) : JewelBridgeException(
"Key '$key' not found in Swing LaF, was expecting a value of type $type",
)
class KeysNotFoundException(keys: List<String>, type: String) : JewelBridgeException(
"Keys ${keys.joinToString(", ") { "'$it'" }} not found in Swing LaF, " +
"was expecting a value of type $type",
)
}
@Suppress("NOTHING_TO_INLINE") // Same implementation as error()
internal inline fun keyNotFound(key: String, type: String): Nothing =
throw JewelBridgeException.KeyNotFoundException(key, type)
@Suppress("NOTHING_TO_INLINE") // Same implementation as error()
internal inline fun keysNotFound(keys: List<String>, type: String): Nothing =
throw JewelBridgeException.KeysNotFoundException(keys, type)

View File

@@ -1,13 +0,0 @@
package org.jetbrains.jewel.bridge
import com.intellij.openapi.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
internal class ProjectLifecycle : Disposable, CoroutineScope {
override val coroutineContext = SupervisorJob()
override fun dispose() = cancel()
}

View File

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

View File

@@ -0,0 +1,92 @@
package org.jetbrains.jewel.bridge
import androidx.compose.ui.text.TextStyle
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.Service.Level
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.ui.NewUI
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.jetbrains.jewel.IntelliJComponentStyling
import org.jetbrains.jewel.IntelliJSvgLoader
import org.jetbrains.jewel.SvgLoader
import org.jetbrains.jewel.themes.PaletteMapperFactory
import org.jetbrains.jewel.themes.intui.core.IntUiThemeDefinition
import org.jetbrains.jewel.themes.intui.core.IntelliJSvgPatcher
@Service(Level.APP)
class SwingBridgeService : Disposable {
private val logger = thisLogger()
// TODO use constructor injection when min IJ is 232+
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + CoroutineName("JewelSwingBridge"))
// TODO we shouldn't assume it's Int UI, but we only have that for now
internal val currentBridgeThemeData: StateFlow<BridgeThemeData> =
IntelliJApplication.lookAndFeelChangedFlow(coroutineScope)
.map { readThemeData() }
.stateIn(coroutineScope, SharingStarted.Eagerly, BridgeThemeData.DEFAULT)
private suspend fun readThemeData(): BridgeThemeData {
val isIntUi = NewUI.isEnabled()
if (!isIntUi) {
// TODO return Darcula/IntelliJ Light theme instead
logger.warn("Darcula LaFs (aka \"old UI\") are not supported yet, falling back to Int UI")
}
val themeDefinition = createBridgeIntUiDefinition()
val svgLoader = createSvgLoader(themeDefinition)
return BridgeThemeData(
themeDefinition = createBridgeIntUiDefinition(),
svgLoader = svgLoader,
componentStyling = createSwingIntUiComponentStyling(themeDefinition, svgLoader),
)
}
override fun dispose() {
coroutineScope.cancel("Disposing Application...")
}
internal data class BridgeThemeData(
val themeDefinition: IntUiThemeDefinition,
val svgLoader: SvgLoader,
val componentStyling: IntelliJComponentStyling,
) {
companion object {
val DEFAULT = run {
val themeDefinition = createBridgeIntUiDefinition(TextStyle.Default)
val svgLoader = createSvgLoader(themeDefinition)
BridgeThemeData(
themeDefinition = createBridgeIntUiDefinition(TextStyle.Default),
svgLoader = createSvgLoader(themeDefinition),
componentStyling = createSwingIntUiComponentStyling(
theme = themeDefinition,
svgLoader = svgLoader,
textAreaTextStyle = TextStyle.Default,
textFieldTextStyle = TextStyle.Default,
dropdownTextStyle = TextStyle.Default,
labelTextStyle = TextStyle.Default,
linkTextStyle = TextStyle.Default,
),
)
}
}
}
}
private fun createSvgLoader(theme: IntUiThemeDefinition): SvgLoader {
val paletteMapper = PaletteMapperFactory.create(theme.isDark, theme.iconData, theme.colorPalette)
val svgPatcher = IntelliJSvgPatcher(paletteMapper)
return IntelliJSvgLoader(svgPatcher)
}

View File

@@ -0,0 +1,19 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.intellij.openapi.components.service
import org.jetbrains.jewel.ExperimentalJewelApi
import org.jetbrains.jewel.themes.intui.standalone.IntUiTheme
private val bridgeService = service<SwingBridgeService>()
@ExperimentalJewelApi
@Composable
fun SwingBridgeTheme(content: @Composable () -> Unit) {
val themeData by bridgeService.currentBridgeThemeData.collectAsState()
// TODO handle non-Int UI themes, too
IntUiTheme(themeData.themeDefinition, themeData.componentStyling, swingCompatMode = true, content)
}

View File

@@ -1,11 +1,11 @@
package org.jetbrains.jewel
package org.jetbrains.jewel.bridge
@Retention(AnnotationRetention.SOURCE)
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.EXPRESSION
AnnotationTarget.EXPRESSION,
)
annotation class SwingLafKey(
val key: String,

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