Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL

# https://youtrack.jetbrains.com/issue/KTIJ-17907
ij_continuation_indent_size = 4
ij_kotlin_indent_before_arrow_on_new_line = true

# Trailing comma
ij_kotlin_allow_trailing_comma_on_call_site = true
Expand Down
27 changes: 27 additions & 0 deletions .run/Run Desktop App.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Desktop App" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/sample/desktop" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-DmainClass=com.flaringapp.compose.topbar.sample.desktop.MainKt --quiet" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<method v="2" />
</configuration>
</component>
362 changes: 362 additions & 0 deletions ComposeCollapsingTopBar/api/jvm/ComposeCollapsingTopBar.api

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions ComposeCollapsingTopBar/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ kotlin {
}
}

jvm()

sourceSets {
commonMain.dependencies {
implementation(libs.compose.multiplatform.foundation)
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

![badge-Android](https://img.shields.io/badge/Platform-Android-forestgreen)
![badge-iOS](https://img.shields.io/badge/Platform-iOS-silver)
![badge-Desktop](https://img.shields.io/badge/Platform-Desktop-dodgerblue)

If you like this project, :star: star and :loudspeaker: share it!

Expand Down Expand Up @@ -739,8 +740,16 @@ This state exposes current exit offset via `exitHeight`.

## More examples

Feel free to install a demo [Android](/sample/android) or [iOS](/sample/ios) app and try yourself
even more samples!
Feel free to install a demo [Android](/sample/android), [iOS](/sample/ios), or
[Desktop](/sample/desktop) app and try yourself even more samples!

## Known issues

- On Desktop, a Compose Multiplatform nested scroll bug can prevent the top bar from expanding
with mouse wheel scrolling when using scroll modes with `expandAlways = false`. See
[CMP-10236](https://youtrack.jetbrains.com/issue/CMP-10236). The sample app includes a
[temporary workaround](/sample/shared/src/jvmMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/desktopfix/DesktopScrollBoundsNestedScrollFix.jvm.kt)
that can be copied until Compose fixes the issue.

## Contributing

Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.jetbrains.kotlin.multiplatform) apply false
alias(libs.plugins.jetbrains.kotlin.compose) apply false
alias(libs.plugins.jetbrains.compose.multiplatform) apply false
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ slack-compose-linter = { group = "com.slack.lint.compose", name = "compose-lint-
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
jetbrains-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
Expand Down
25 changes: 25 additions & 0 deletions sample/desktop/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
alias(libs.plugins.jetbrains.kotlin.jvm)
alias(libs.plugins.jetbrains.kotlin.compose)
alias(libs.plugins.jetbrains.compose.multiplatform)
}

dependencies {
implementation(project(":sample:shared"))

implementation(compose.desktop.currentOs)
}

compose.desktop {
application {
mainClass = "com.flaringapp.compose.topbar.sample.desktop.MainKt"

nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.flaringapp.compose.topbar.sample.desktop"
packageVersion = "1.0.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Flaringapp
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.flaringapp.compose.topbar.sample.desktop

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.flaringapp.compose.topbar.sample.shared.App

fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "ComposeCollapsingTopBar",
) {
App()
}
}
2 changes: 2 additions & 0 deletions sample/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ kotlin {
}
}

jvm()

sourceSets {
commonMain.dependencies {
implementation(project(":ComposeCollapsingTopBar"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2026 Flaringapp
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.flaringapp.compose.topbar.sample.shared.ui.samples.desktopfix

import androidx.compose.ui.Modifier
import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffoldState

internal actual fun Modifier.desktopScrollBoundsNestedScrollFix(
state: CollapsingTopBarScaffoldState,
isContentAtTop: () -> Boolean,
): Modifier = this
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2026 Flaringapp
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.flaringapp.compose.topbar.sample.shared.ui.samples.desktopfix

import androidx.compose.ui.Modifier
import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffoldState

internal actual fun Modifier.desktopScrollBoundsNestedScrollFix(
state: CollapsingTopBarScaffoldState,
isContentAtTop: () -> Boolean,
): Modifier = this
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.flaringapp.compose.topbar.sample.shared.ui.samples.basic

import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand All @@ -29,9 +30,11 @@ import com.flaringapp.compose.topbar.sample.shared.ui.samples.CollapsingTopBarSa
import com.flaringapp.compose.topbar.sample.shared.ui.samples.common.SampleContent
import com.flaringapp.compose.topbar.sample.shared.ui.samples.common.SampleTopAppBar
import com.flaringapp.compose.topbar.sample.shared.ui.samples.common.SampleTopBarImage
import com.flaringapp.compose.topbar.sample.shared.ui.samples.desktopfix.desktopScrollBoundsNestedScrollFix
import com.flaringapp.compose.topbar.sample.shared.ui.theme.ComposeCollapsingTopBarTheme
import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffold
import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffoldScrollMode
import com.flaringapp.compose.topbar.scaffold.rememberCollapsingTopBarScaffoldState

private const val SCRIM_START_FRACTION = 0.25f

Expand All @@ -42,8 +45,15 @@ fun BasicScaffoldSampleContent(
scrollMode: CollapsingTopBarScaffoldScrollMode,
modifier: Modifier = Modifier,
) {
val scaffoldState = rememberCollapsingTopBarScaffoldState()
val contentScrollState = rememberScrollState()

CollapsingTopBarScaffold(
modifier = modifier,
modifier = modifier.desktopScrollBoundsNestedScrollFix(
state = scaffoldState,
isContentAtTop = { contentScrollState.value == 0 },
),
state = scaffoldState,
scrollMode = scrollMode,
topBar = {
var topBarColorProgress by remember { mutableFloatStateOf(1f) }
Expand All @@ -66,7 +76,9 @@ fun BasicScaffoldSampleContent(
)
},
body = {
SampleContent()
SampleContent(
scrollState = contentScrollState,
)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.flaringapp.compose.topbar.sample.shared.ui.samples.common

import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
Expand All @@ -39,12 +40,13 @@ import com.flaringapp.compose.topbar.sample.shared.ui.theme.ComposeCollapsingTop
@Composable
inline fun SampleContent(
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
before: ColumnScope.() -> Unit = {},
) {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
.verticalScroll(scrollState),
) {
before()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.flaringapp.compose.topbar.sample.shared.ui.samples.common

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -47,10 +48,12 @@ fun SampleTopBarImage(
height: Dp = 300.dp,
) {
Image(
modifier = modifier.height(height),
modifier = modifier
.fillMaxWidth()
.height(height),
painter = painterResource(dog.imageRes),
contentDescription = null,
contentScale = ContentScale.FillHeight,
contentScale = ContentScale.Crop,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2026 Flaringapp
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.flaringapp.compose.topbar.sample.shared.ui.samples.desktopfix

import androidx.compose.ui.Modifier
import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffoldState

/**
* Sample-only workaround for CMP-10236.
*
* https://youtrack.jetbrains.com/issue/CMP-10236
*
* Compose Desktop can skip parent nested-scroll `onPostScroll` when scrollable content is already
* at its bounds. This modifier forwards that leftover wheel delta to the scaffold state until
* Compose handles the case upstream.
*/
internal expect fun Modifier.desktopScrollBoundsNestedScrollFix(
state: CollapsingTopBarScaffoldState,
isContentAtTop: () -> Boolean,
): Modifier
Loading
Loading