Skip to content

Commit

Permalink
multiplatform: Add desktop support (WIP)
Browse files Browse the repository at this point in the history
Co-authored-by: Patryk Goworowski <[email protected]>
  • Loading branch information
patrickmichalik and Gowsky committed Feb 19, 2025
1 parent fda4d2a commit e16ef8d
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 52 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.externalNativeBuild
.gradle
.kotlin
/.idea/.artifacts/
/.idea/AndroidProjectSystem.xml
/.idea/assetWizardSettings.xml
/.idea/caches
Expand Down
14 changes: 14 additions & 0 deletions sample/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
Expand Down Expand Up @@ -53,6 +54,7 @@ kotlin {
isStatic = true
}
}
jvm("desktop")
sourceSets {
androidMain.dependencies {
implementation(libs.activityCompose)
Expand All @@ -66,5 +68,17 @@ kotlin {
implementation(libs.composeNavigation)
implementation(project(":sample:multiplatform"))
}
val desktopMain by getting
desktopMain.dependencies { implementation(compose.desktop.currentOs) }
}
}

compose.desktop {
application {
mainClass = "com.patrykandpatrick.vico.sample.MainKt"
nativeDistributions {
packageName = "com.patrykandpatrick.vico.sample"
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2025 by Patryk Goworowski and Patrick Michalik.
*
* 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.patrykandpatrick.vico.sample

actual val Charts.overridden: LinkedHashMap<UIFramework, List<Chart>>?
get() = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2025 by Patryk Goworowski and Patrick Michalik.
*
* 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.patrykandpatrick.vico.sample

import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() {
application { Window(onCloseRequest = ::exitApplication, title = "Vico") { SampleApp() } }
}
1 change: 1 addition & 0 deletions sample/multiplatform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ kotlin {
isStatic = true
}
}
jvm("desktop")
sourceSets {
commonMain.dependencies {
implementation(compose.foundation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,12 @@ internal fun CartesianChartHostImpl(
Canvas(
modifier =
Modifier.fillMaxSize()
.chartTouchEvent(
setTouchPoint =
.pointerInput(
scrollState = scrollState,
onPointerPositionChange =
remember(chart.marker == null) {
if (chart.marker != null) pointerPosition.component2() else null
},
isScrollEnabled = scrollState.scrollEnabled,
scrollState = scrollState,
onZoom =
remember(zoomState, scrollState, chart, coroutineScope) {
if (zoomState.zoomEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,31 @@ import androidx.compose.ui.input.pointer.pointerInput
import com.patrykandpatrick.vico.compose.common.detectZoomGestures
import com.patrykandpatrick.vico.core.common.Point

internal fun Modifier.chartTouchEvent(
setTouchPoint: ((Point?) -> Unit)?,
isScrollEnabled: Boolean,
private fun Offset.toPoint() = Point(x, y)

internal fun Modifier.pointerInput(
scrollState: VicoScrollState,
onPointerPositionChange: ((Point?) -> Unit)?,
onZoom: ((Float, Offset) -> Unit)?,
): Modifier =
scrollable(
state = scrollState.scrollableState,
orientation = Orientation.Horizontal,
enabled = scrollState.scrollEnabled,
reverseDirection = true,
enabled = isScrollEnabled,
)
.then(
if (setTouchPoint != null) {
pointerInput(setTouchPoint) {
if (onPointerPositionChange != null) {
Modifier.pointerInput(onPointerPositionChange) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Press -> setTouchPoint(event.changes.first().position.point)
PointerEventType.Release -> setTouchPoint(null)
PointerEventType.Move ->
if (!isScrollEnabled) setTouchPoint(event.changes.first().position.point)
when {
event.type == PointerEventType.Press ->
onPointerPositionChange(event.changes[0].position.toPoint())
event.type == PointerEventType.Release -> onPointerPositionChange(null)
event.type == PointerEventType.Move && !scrollState.scrollEnabled ->
onPointerPositionChange(event.changes[0].position.toPoint())
}
}
}
Expand All @@ -57,17 +59,14 @@ internal fun Modifier.chartTouchEvent(
}
)
.then(
if (isScrollEnabled && onZoom != null) {
pointerInput(setTouchPoint, onZoom) {
if (scrollState.scrollEnabled && onZoom != null) {
Modifier.pointerInput(onPointerPositionChange, onZoom) {
detectZoomGestures { centroid, zoom ->
setTouchPoint?.invoke(null)
onPointerPositionChange?.invoke(null)
onZoom(zoom, centroid)
}
}
} else {
Modifier
}
)

private val Offset.point: Point
get() = Point(x, y)
1 change: 1 addition & 0 deletions vico/multiplatform-m2/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ kotlin {
isStatic = true
}
}
jvm("desktop")
sourceSets {
commonMain.dependencies {
api(project(":vico:multiplatform"))
Expand Down
1 change: 1 addition & 0 deletions vico/multiplatform-m3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ kotlin {
isStatic = true
}
}
jvm("desktop")
sourceSets {
commonMain.dependencies {
api(project(":vico:multiplatform"))
Expand Down
1 change: 1 addition & 0 deletions vico/multiplatform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ kotlin {
isStatic = true
}
}
jvm("desktop")
sourceSets {
commonMain.dependencies {
implementation(compose.foundation)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2025 by Patryk Goworowski and Patrick Michalik.
*
* 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.patrykandpatrick.vico.multiplatform.cartesian

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable internal actual fun Modifier.extraPointerInput(scrollState: VicoScrollState) = this
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,12 @@ internal fun CartesianChartHostImpl(
Canvas(
modifier =
Modifier.fillMaxSize()
.chartTouchEvent(
setTouchPoint =
.pointerInput(
scrollState = scrollState,
onPointerPositionChange =
remember(chart.marker == null) {
if (chart.marker != null) pointerPosition.component2() else null
},
isScrollEnabled = scrollState.scrollEnabled,
scrollState = scrollState,
onZoom =
remember(zoomState, scrollState, chart, coroutineScope) {
if (zoomState.zoomEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,56 +18,62 @@ package com.patrykandpatrick.vico.multiplatform.cartesian

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import com.patrykandpatrick.vico.multiplatform.common.Point
import com.patrykandpatrick.vico.multiplatform.common.detectZoomGestures

internal fun Modifier.chartTouchEvent(
setTouchPoint: ((Point?) -> Unit)?,
isScrollEnabled: Boolean,
private const val BASE_SCROLL_ZOOM_DELTA = 0.1f

private fun Offset.toPoint() = Point(x, y)

@Composable internal expect fun Modifier.extraPointerInput(scrollState: VicoScrollState): Modifier

@Composable
internal fun Modifier.pointerInput(
scrollState: VicoScrollState,
onPointerPositionChange: ((Point?) -> Unit)?,
onZoom: ((Float, Offset) -> Unit)?,
): Modifier =
) =
scrollable(
state = scrollState.scrollableState,
orientation = Orientation.Horizontal,
enabled = scrollState.scrollEnabled,
reverseDirection = true,
enabled = isScrollEnabled,
)
.then(
if (setTouchPoint != null) {
pointerInput(setTouchPoint) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Press -> setTouchPoint(event.changes.first().position.point)
PointerEventType.Release -> setTouchPoint(null)
PointerEventType.Move ->
if (!isScrollEnabled) setTouchPoint(event.changes.first().position.point)
}
}
.pointerInput(onZoom, onPointerPositionChange) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when {
event.type == PointerEventType.Scroll && scrollState.scrollEnabled && onZoom != null ->
onZoom(
1 - event.changes[0].scrollDelta.y * BASE_SCROLL_ZOOM_DELTA,
event.changes[0].position,
)
onPointerPositionChange == null -> continue
event.type == PointerEventType.Press ->
onPointerPositionChange(event.changes[0].position.toPoint())
event.type == PointerEventType.Release -> onPointerPositionChange(null)
event.type == PointerEventType.Move && !scrollState.scrollEnabled ->
onPointerPositionChange(event.changes[0].position.toPoint())
}
}
} else {
Modifier
}
)
}
.then(
if (isScrollEnabled && onZoom != null) {
pointerInput(setTouchPoint, onZoom) {
if (scrollState.scrollEnabled && onZoom != null) {
Modifier.pointerInput(onPointerPositionChange, onZoom) {
detectZoomGestures { centroid, zoom ->
setTouchPoint?.invoke(null)
onPointerPositionChange?.invoke(null)
onZoom(zoom, centroid)
}
}
} else {
Modifier
}
)

private val Offset.point: Point
get() = Point(x, y)
.extraPointerInput(scrollState)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2025 by Patryk Goworowski and Patrick Michalik.
*
* 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.patrykandpatrick.vico.multiplatform.cartesian

import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import com.patrykandpatrick.vico.multiplatform.common.getValue
import com.patrykandpatrick.vico.multiplatform.common.rememberWrappedValue
import com.patrykandpatrick.vico.multiplatform.common.setValue
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

@Composable
internal actual fun Modifier.extraPointerInput(scrollState: VicoScrollState): Modifier {
var scrollJob by rememberWrappedValue<Job?>(null)
val coroutineScope = rememberCoroutineScope()
val animationSpec = rememberSplineBasedDecay<Float>()
return draggable(
state =
rememberDraggableState { delta ->
scrollJob?.cancel()
coroutineScope.launch { scrollState.scroll(Scroll.Relative.pixels(delta)) }
},
orientation = Orientation.Horizontal,
onDragStopped = { velocity ->
scrollJob =
coroutineScope.launch {
AnimationState(scrollState.value, velocity).animateDecay(animationSpec) {
runBlocking { scrollState.scroll(Scroll.Absolute.pixels(value)) }
}
}
},
reverseDirection = true,
)
}
Loading

0 comments on commit e16ef8d

Please sign in to comment.