diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt index 325dd02f41..7b1e44dc16 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt @@ -6,6 +6,19 @@ @file:Suppress("UNUSED", "NOTHING_TO_INLINE", "FunctionName") package org.jetbrains.compose.web.css +import org.w3c.dom.css.CSSRule +import org.w3c.dom.css.CSSRuleList + + +external class CSSKeyframesRule: CSSRule { + val name: String + val cssRules: CSSRuleList +} + +inline fun CSSKeyframesRule.appendRule(cssRule: String) { + this.asDynamic().appendRule(cssRule) +} + @Suppress("NOTHING_TO_INLINE") inline fun jsObject(): T = js("({})") diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt index aa7b96a500..5d87857b97 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt @@ -181,3 +181,74 @@ external interface Position: StylePropertyEnum { inline fun Position(value: String) = value.unsafeCast() typealias LanguageCode = String + +external interface StepPosition: StylePropertyEnum { + companion object { + inline val JumpStart get() = StepPosition("jump-start") + inline val JumpEnd get() = StepPosition("jump-end") + inline val JumpNone get() = StepPosition("jump-none") + inline val JumpBoth get() = StepPosition("jump-both") + inline val Start get() = StepPosition("start") + inline val End get() = StepPosition("end") + } +} +inline fun StepPosition(value: String) = value.unsafeCast() + +external interface AnimationTimingFunction: StylePropertyEnum { + companion object { + inline val Ease get() = AnimationTimingFunction("ease") + inline val EaseIn get() = AnimationTimingFunction("ease-in") + inline val EaseOut get() = AnimationTimingFunction("ease-out") + inline val EaseInOut get() = AnimationTimingFunction("ease-in-out") + inline val Linear get() = AnimationTimingFunction("linear") + inline val StepStart get() = AnimationTimingFunction("step-start") + inline val StepEnd get() = AnimationTimingFunction("step-end") + + inline fun cubicBezier(x1: Double, y1: Double, x2: Double, y2: Double) = AnimationTimingFunction("cubic-bezier($x1, $y1, $x2, $y2)") + inline fun steps(count: Int, stepPosition: StepPosition) = AnimationTimingFunction("steps($count, $stepPosition)") + inline fun steps(count: Int) = AnimationTimingFunction("steps($count)") + + inline val Inherit get() = AnimationTimingFunction("inherit") + inline val Initial get() = AnimationTimingFunction("initial") + inline val Unset get() = AnimationTimingFunction("unset") + } +} +inline fun AnimationTimingFunction(value: String) = value.unsafeCast() + +external interface AnimationDirection: StylePropertyEnum { + companion object { + inline val Normal get() = AnimationDirection("normal") + inline val Reverse get() = AnimationDirection("reverse") + inline val Alternate get() = AnimationDirection("alternate") + inline val AlternateReverse get() = AnimationDirection("alternate-reverse") + + inline val Inherit get() = AnimationDirection("inherit") + inline val Initial get() = AnimationDirection("initial") + inline val Unset get() = AnimationDirection("unset") + } +} +inline fun AnimationDirection(value: String) = value.unsafeCast() + +external interface AnimationFillMode: StylePropertyEnum { + companion object { + inline val None get() = AnimationFillMode("none") + inline val Forwards get() = AnimationFillMode("forwards") + inline val Backwards get() = AnimationFillMode("backwards") + inline val Both get() = AnimationFillMode("both") + } +} +inline fun AnimationFillMode(value: String) = value.unsafeCast() + +external interface AnimationPlayState: StylePropertyEnum { + companion object { + inline val Running get() = AnimationPlayState("running") + inline val Paused get() = AnimationPlayState("Paused") + inline val Backwards get() = AnimationPlayState("backwards") + inline val Both get() = AnimationPlayState("both") + + inline val Inherit get() = AnimationPlayState("inherit") + inline val Initial get() = AnimationPlayState("initial") + inline val Unset get() = AnimationPlayState("unset") + } +} +inline fun AnimationPlayState(value: String) = value.unsafeCast() diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSKeyframeRule.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSKeyframeRule.kt new file mode 100644 index 0000000000..37ef76fcef --- /dev/null +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSKeyframeRule.kt @@ -0,0 +1,73 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package org.jetbrains.compose.web.css + +interface CSSNamedKeyframes { + val name: String +} + +data class CSSKeyframesRuleDeclaration( + override val name: String, + val keys: CSSKeyframeRuleDeclarationList +) : CSSRuleDeclaration, CSSNamedKeyframes { + override val header: String + get() = "@keyframes $name" +} + +typealias CSSKeyframeRuleDeclarationList = List + +abstract class CSSKeyframe { + abstract override fun toString(): String + + object From: CSSKeyframe() { + override fun toString(): String = "from" + } + + object To: CSSKeyframe() { + override fun toString(): String = "to" + } + + data class Percentage(val value: CSSSizeValue): CSSKeyframe() { + override fun toString(): String = value.toString() + } + + data class Combine(val values: List>): CSSKeyframe() { + override fun toString(): String = values.joinToString(", ") + } +} + +data class CSSKeyframeRuleDeclaration( + val keyframe: CSSKeyframe, + override val style: StyleHolder +) : CSSRuleDeclaration, CSSStyledRuleDeclaration { + override val header: String + get() = keyframe.toString() +} + +class CSSKeyframesBuilder() { + constructor(init: CSSKeyframesBuilder.() -> Unit) : this() { + init() + } + val frames: MutableList = mutableListOf() + + fun from(style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.From, buildCSSStyleRule(style)) + } + + fun to(style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.To, buildCSSStyleRule(style)) + } + + fun each(vararg keys: CSSSizeValue, style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.Combine(keys.toList()), buildCSSStyleRule(style)) + } + + operator fun CSSSizeValue.invoke(style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.Percentage(this), buildCSSStyleRule(style)) + } +} + +fun buildKeyframes(name: String, builder: CSSKeyframesBuilder.() -> Unit): CSSKeyframesRuleDeclaration { + val frames = CSSKeyframesBuilder(builder).frames + return CSSKeyframesRuleDeclaration(name, frames) +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt index b25d447dd4..97d4367c07 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt @@ -18,7 +18,7 @@ interface CSSMediaQuery { } @Suppress("EqualsOrHashCode") - data class MediaFeature( + class MediaFeature( val name: String, val value: StylePropertyValue? = null ) : CSSMediaQuery, Atomic { @@ -61,8 +61,8 @@ interface CSSMediaQuery { @Suppress("EqualsOrHashCode") class CSSMediaRuleDeclaration( val query: CSSMediaQuery, - rules: CSSRuleDeclarationList -) : CSSGroupingRuleDeclaration(rules) { + override val rules: CSSRuleDeclarationList +) : CSSGroupingRuleDeclaration { override val header: String get() = "@media $query" diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt index 32d56f27a5..8f4c4746a2 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt @@ -237,3 +237,70 @@ fun StyleBuilder.padding(value: CSSNumeric) { // padding hasn't Typed OM yet property("padding", value) } + +@Suppress("EqualsOrHashCode") +data class CSSAnimation( + val keyframesName: String, + var duration: List>? = null, + var timingFunction: List? = null, + var delay: List>? = null, + var iterationCount: List? = null, + var direction: List? = null, + var fillMode: List? = null, + var playState: List? = null +) : CSSStyleValue { + override fun toString(): String { + val values = listOfNotNull( + keyframesName, + duration?.joinToString(", "), + timingFunction?.joinToString(", "), + delay?.joinToString(", "), + iterationCount?.joinToString(", ") { it?.toString() ?: "infinite" }, + direction?.joinToString(", "), + fillMode?.joinToString(", "), + playState?.joinToString(", ") + ) + return values.joinToString(" ") + } +} + +inline fun CSSAnimation.duration(vararg values: CSSSizeValue) { + this.duration = values.toList() +} + +inline fun CSSAnimation.timingFunction(vararg values: AnimationTimingFunction) { + this.timingFunction = values.toList() +} + +inline fun CSSAnimation.delay(vararg values: CSSSizeValue) { + this.delay = values.toList() +} + +inline fun CSSAnimation.iterationCount(vararg values: Int?) { + this.iterationCount = values.toList() +} + +inline fun CSSAnimation.direction(vararg values: AnimationDirection) { + this.direction = values.toList() +} + +inline fun CSSAnimation.fillMode(vararg values: AnimationFillMode) { + this.fillMode = values.toList() +} + +inline fun CSSAnimation.playState(vararg values: AnimationPlayState) { + this.playState = values.toList() +} + +fun StyleBuilder.animation( + keyframesName: String, + builder: CSSAnimation.() -> Unit +) { + val animation = CSSAnimation(keyframesName).apply(builder) + property("animation", animation) +} + +inline fun StyleBuilder.animation( + keyframes: CSSNamedKeyframes, + noinline builder: CSSAnimation.() -> Unit +) = animation(keyframes.name, builder) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt index 532655e3a7..515a81d42f 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt @@ -6,23 +6,28 @@ interface CSSStyleRuleBuilder : StyleBuilder open class CSSRuleBuilderImpl : CSSStyleRuleBuilder, StyleBuilderImpl() -abstract class CSSRuleDeclaration { - abstract val header: String +@Suppress("EqualsOrHashCode") +interface CSSRuleDeclaration { + val header: String - abstract override fun equals(other: Any?): Boolean + override fun equals(other: Any?): Boolean +} + +interface CSSStyledRuleDeclaration { + val style: StyleHolder } data class CSSStyleRuleDeclaration( val selector: CSSSelector, - val style: StyleHolder -) : CSSRuleDeclaration() { + override val style: StyleHolder +) : CSSRuleDeclaration, CSSStyledRuleDeclaration { override val header get() = selector.toString() } -abstract class CSSGroupingRuleDeclaration( +interface CSSGroupingRuleDeclaration: CSSRuleDeclaration { val rules: CSSRuleDeclarationList -) : CSSRuleDeclaration() +} typealias CSSRuleDeclarationList = List typealias MutableCSSRuleDeclarationList = MutableList diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt index a6b7c64b0e..2cce399fff 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt @@ -14,6 +14,7 @@ class CSSRulesHolderState : CSSRulesHolder { override var cssRules: CSSRuleDeclarationList by mutableStateOf(listOf()) override fun add(cssRule: CSSRuleDeclaration) { + @Suppress("SuspiciousCollectionReassignment") cssRules += cssRule } } @@ -39,17 +40,21 @@ class CSSRulesHolderState : CSSRulesHolder { * ``` */ open class StyleSheet( - private val rulesHolder: CSSRulesHolder = CSSRulesHolderState() + private val rulesHolder: CSSRulesHolder = CSSRulesHolderState(), + val usePrefix: Boolean = true, ) : StyleSheetBuilder, CSSRulesHolder by rulesHolder { private val boundClasses = mutableMapOf() - protected fun style(cssRule: CSSBuilder.() -> Unit) = CSSHolder(cssRule) + protected fun style(cssRule: CSSBuilder.() -> Unit) = CSSHolder(usePrefix, cssRule) + + protected fun keyframes(cssKeyframes: CSSKeyframesBuilder.() -> Unit) = CSSKeyframesHolder(usePrefix, cssKeyframes) companion object { var counter = 0 } - data class CSSSelfSelector(var selector: CSSSelector? = null) : CSSSelector() { + @Suppress("EqualsOrHashCode") + class CSSSelfSelector(var selector: CSSSelector? = null) : CSSSelector() { override fun toString(): String = selector.toString() override fun equals(other: Any?): Boolean { return other is CSSSelfSelector @@ -77,12 +82,12 @@ open class StyleSheet( } } - protected class CSSHolder(val cssBuilder: CSSBuilder.() -> Unit) { + protected class CSSHolder(private val usePrefix: Boolean, private val cssBuilder: CSSBuilder.() -> Unit) { operator fun provideDelegate( sheet: StyleSheet, property: KProperty<*> ): ReadOnlyProperty { - val sheetName = "${sheet::class.simpleName}-" + val sheetName = if (usePrefix) "${sheet::class.simpleName}-" else "" val selector = className("$sheetName${property.name}") val (properties, rules) = buildCSS(selector, selector, cssBuilder) sheet.add(selector, properties) @@ -94,6 +99,22 @@ open class StyleSheet( } } + protected class CSSKeyframesHolder(private val usePrefix: Boolean, private val keyframesBuilder: CSSKeyframesBuilder.() -> Unit) { + operator fun provideDelegate( + sheet: StyleSheet, + property: KProperty<*> + ): ReadOnlyProperty { + val sheetName = if (usePrefix) "${sheet::class.simpleName}-" else "" + val keyframesName = "$sheetName${property.name}" + val rule = buildKeyframes(keyframesName, keyframesBuilder) + sheet.add(rule) + + return ReadOnlyProperty { _, _ -> + rule + } + } + } + override fun buildRules(rulesBuild: GenericStyleSheetBuilder.() -> Unit) = StyleSheet().apply(rulesBuild).cssRules } diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Style.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Style.kt index 2837890f0a..2411cc1f4c 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Style.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Style.kt @@ -24,6 +24,11 @@ private fun CSSStyleSheet.addRule(cssRule: String): CSSRule? { return this.cssRules.item(cssRuleIndex) } +private fun CSSKeyframesRule.addRule(cssRule: String): CSSRule? { + appendRule(cssRule) + return this.cssRules.item(this.cssRules.length - 1) +} + private fun CSSStyleSheet.addRule(cssRuleDeclaration: CSSRuleDeclaration) { addRule("${cssRuleDeclaration.header} {}")?.let { cssRule -> fillRule(cssRuleDeclaration, cssRule) @@ -41,12 +46,18 @@ private fun CSSGroupingRule.addRule(cssRuleDeclaration: CSSRuleDeclaration) { } } +private fun CSSKeyframesRule.addRule(cssRuleDeclaration: CSSKeyframeRuleDeclaration) { + addRule("${cssRuleDeclaration.header} {}")?.let { cssRule -> + fillRule(cssRuleDeclaration, cssRule) + } +} + private fun fillRule( cssRuleDeclaration: CSSRuleDeclaration, cssRule: CSSRule ) { when (cssRuleDeclaration) { - is CSSStyleRuleDeclaration -> { + is CSSStyledRuleDeclaration -> { val cssStyleRule = cssRule.unsafeCast() cssRuleDeclaration.style.properties.forEach { (name, value) -> setProperty(cssStyleRule.style, name, value) @@ -61,6 +72,12 @@ private fun fillRule( cssGroupingRule.addRule(childRuleDeclaration) } } + is CSSKeyframesRuleDeclaration -> { + val cssGroupingRule = cssRule.unsafeCast() + cssRuleDeclaration.keys.forEach { childRuleDeclaration -> + cssGroupingRule.addRule(childRuleDeclaration) + } + } } } diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt index e6609db73e..4aaee219a8 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt @@ -44,8 +44,23 @@ object MyCSSVariables : CSSVariables { } object AppStyleSheet : StyleSheet() { + val bounce by keyframes { + from { + property("transform", "translateX(50%)") + } + + to { + property("transform", "translateX(-50%)") + } + } + val myClass by style { color("green") + animation(bounce) { + duration(2.s) + timingFunction(AnimationTimingFunction.EaseIn) + direction(AnimationDirection.Alternate) + } } val classWithNested by style {