├── screenshot.png ├── m2 ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── windedge │ │ │ └── table │ │ │ └── m2 │ │ │ ├── PaginatedDataTable.kt │ │ │ └── Paginator.kt │ └── jvmMain │ │ └── kotlin │ │ └── Preview.kt ├── gradle.properties └── build.gradle.kts ├── m3 ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── windedge │ │ │ └── table │ │ │ └── m3 │ │ │ ├── PaginatedDataTable.kt │ │ │ └── Paginator.kt │ └── jvmMain │ │ └── kotlin │ │ └── Preview.kt ├── gradle.properties └── build.gradle.kts ├── common ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ ├── composeResources │ │ └── drawable │ │ │ ├── arrow_left.xml │ │ │ ├── arrow_right.xml │ │ │ ├── skip_left.xml │ │ │ └── skip_right.xml │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── windedge │ │ └── table │ │ ├── components │ │ └── Divider.kt │ │ ├── TableDslBuilder.kt │ │ ├── BasicPaginatedDataTable.kt │ │ └── DataTable.kt ├── gradle.properties └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── sample ├── gradle │ ├── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ └── libs.versions.toml ├── composeApp │ ├── src │ │ ├── jsMain │ │ │ ├── kotlin │ │ │ │ ├── compose │ │ │ │ │ └── table │ │ │ │ │ │ └── demo │ │ │ │ │ │ ├── App.js.kt │ │ │ │ │ │ └── theme │ │ │ │ │ │ └── Theme.js.kt │ │ │ │ └── main.kt │ │ │ └── resources │ │ │ │ └── index.html │ │ ├── wasmJsMain │ │ │ ├── kotlin │ │ │ │ ├── compose │ │ │ │ │ └── table │ │ │ │ │ │ └── demo │ │ │ │ │ │ ├── App.js.kt │ │ │ │ │ │ └── theme │ │ │ │ │ │ └── Theme.js.kt │ │ │ │ └── main.kt │ │ │ └── resources │ │ │ │ └── index.html │ │ ├── jvmMain │ │ │ └── kotlin │ │ │ │ ├── compose │ │ │ │ └── table │ │ │ │ │ └── demo │ │ │ │ │ ├── theme │ │ │ │ │ └── Theme.jvm.kt │ │ │ │ │ └── App.jvm.kt │ │ │ │ └── main.kt │ │ ├── iosMain │ │ │ └── kotlin │ │ │ │ ├── main.kt │ │ │ │ └── compose │ │ │ │ └── table │ │ │ │ └── demo │ │ │ │ └── App.ios.kt │ │ ├── androidMain │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ │ └── compose │ │ │ │ └── table │ │ │ │ └── demo │ │ │ │ ├── theme │ │ │ │ └── Theme.android.kt │ │ │ │ └── App.android.kt │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── compose │ │ │ └── table │ │ │ └── demo │ │ │ ├── theme │ │ │ ├── Color.kt │ │ │ └── Theme.kt │ │ │ └── App.kt │ ├── webpack.config.d │ │ └── config.js │ └── build.gradle.kts ├── build.gradle.kts ├── gradle.properties ├── README.md ├── settings.gradle.kts ├── gradlew.bat └── gradlew ├── settings.gradle.kts ├── .gitignore ├── LICENSE ├── gradle.properties ├── .github ├── workflows │ ├── publish.yml │ └── release.yml └── GUIDE.md ├── README.md ├── gradlew.bat └── gradlew /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/windedge/compose-table/HEAD/screenshot.png -------------------------------------------------------------------------------- /m2/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /m3/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /common/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/windedge/compose-table/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/windedge/compose-table/HEAD/sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /common/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=table 2 | POM_NAME=Compose Table Component 3 | 4 | kotlin.mpp.androidSourceSetLayoutVersion=2 5 | -------------------------------------------------------------------------------- /m2/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=table-m2 2 | POM_NAME=Compose Table Component for Material 2 3 | 4 | kotlin.mpp.androidSourceSetLayoutVersion=2 5 | -------------------------------------------------------------------------------- /m3/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=table-m3 2 | POM_NAME=Compose Table Component for Material 3 3 | 4 | kotlin.mpp.androidSourceSetLayoutVersion=2 5 | -------------------------------------------------------------------------------- /sample/composeApp/src/jsMain/kotlin/compose/table/demo/App.js.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo 2 | 3 | import kotlinx.browser.window 4 | 5 | internal actual fun openUrl(url: String?) { 6 | url?.let { window.open(it) } 7 | } -------------------------------------------------------------------------------- /sample/composeApp/src/wasmJsMain/kotlin/compose/table/demo/App.js.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo 2 | 3 | import kotlinx.browser.window 4 | 5 | internal actual fun openUrl(url: String?) { 6 | url?.let { window.open(it) } 7 | } -------------------------------------------------------------------------------- /sample/composeApp/src/jsMain/kotlin/compose/table/demo/theme/Theme.js.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun SystemAppearance(isDark: Boolean) { 7 | } -------------------------------------------------------------------------------- /sample/composeApp/src/jvmMain/kotlin/compose/table/demo/theme/Theme.jvm.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun SystemAppearance(isDark: Boolean) { 7 | } -------------------------------------------------------------------------------- /sample/composeApp/src/wasmJsMain/kotlin/compose/table/demo/theme/Theme.js.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun SystemAppearance(isDark: Boolean) { 7 | } -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.multiplatform).apply(false) 3 | alias(libs.plugins.compose).apply(false) 4 | alias(libs.plugins.compiler.compose).apply(false) 5 | alias(libs.plugins.android.application).apply(false) 6 | } 7 | -------------------------------------------------------------------------------- /sample/composeApp/src/iosMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import compose.table.demo.App 3 | import platform.UIKit.UIViewController 4 | 5 | fun MainViewController(): UIViewController = ComposeUIViewController { App() } 6 | -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /sample/composeApp/src/jvmMain/kotlin/compose/table/demo/App.jvm.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo 2 | 3 | import java.awt.Desktop 4 | import java.net.URI 5 | 6 | internal actual fun openUrl(url: String?) { 7 | val uri = url?.let { URI.create(it) } ?: return 8 | Desktop.getDesktop().browse(uri) 9 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | mavenCentral() 7 | google() 8 | } 9 | } 10 | 11 | include( 12 | ":common", 13 | ":m2", 14 | ":m3", 15 | ) 16 | 17 | rootProject.name = "compose-table" 18 | -------------------------------------------------------------------------------- /sample/composeApp/src/iosMain/kotlin/compose/table/demo/App.ios.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo 2 | 3 | import platform.Foundation.NSURL 4 | import platform.UIKit.UIApplication 5 | 6 | internal actual fun openUrl(url: String?) { 7 | val nsUrl = url?.let { NSURL.URLWithString(it) } ?: return 8 | UIApplication.sharedApplication.openURL(nsUrl) 9 | } 10 | -------------------------------------------------------------------------------- /sample/composeApp/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.CanvasBasedWindow 3 | import compose.table.demo.App 4 | 5 | 6 | @OptIn(ExperimentalComposeUiApi::class) 7 | fun main() { 8 | CanvasBasedWindow(canvasElementId = "ComposeTarget") { 9 | App() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample/composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compose Table Demo 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /sample/composeApp/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sample 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.jetbrains.compose.experimental.jscanvas.enabled=true 6 | 7 | #Kotlin 8 | kotlin.code.style=official 9 | kotlin.js.compiler=ir 10 | 11 | #Android 12 | android.useAndroidX=true 13 | android.nonTransitiveRClass=true 14 | 15 | -------------------------------------------------------------------------------- /sample/composeApp/src/jsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.CanvasBasedWindow 3 | import compose.table.demo.App 4 | import org.jetbrains.skiko.wasm.onWasmReady 5 | 6 | @OptIn(ExperimentalComposeUiApi::class) 7 | fun main() { 8 | onWasmReady { 9 | CanvasBasedWindow(canvasElementId = "ComposeTarget") { 10 | App() 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/commonMain/composeResources/drawable/arrow_left.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /common/src/commonMain/composeResources/drawable/arrow_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /common/src/commonMain/composeResources/drawable/skip_left.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /common/src/commonMain/composeResources/drawable/skip_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /sample/composeApp/webpack.config.d/config.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require("terser-webpack-plugin"); 2 | 3 | config.optimization = config.optimization || {}; 4 | config.optimization.minimize = true; 5 | config.optimization.minimizer = [ 6 | new TerserPlugin({ 7 | terserOptions: { 8 | mangle: true, // Note: By default, mangle is set to true. 9 | compress: false, // Disable the transformations that reduce the code size. 10 | output: { 11 | beautify: false, 12 | }, 13 | }, 14 | }), 15 | ]; -------------------------------------------------------------------------------- /sample/composeApp/src/jvmMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.dp 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import androidx.compose.ui.window.rememberWindowState 5 | import java.awt.Dimension 6 | import compose.table.demo.App 7 | 8 | fun main() = application { 9 | Window( 10 | title = "Compose Table Demo", 11 | state = rememberWindowState(width = 800.dp, height = 600.dp), 12 | onCloseRequest = ::exitApplication, 13 | ) { 14 | window.minimumSize = Dimension(600, 350) 15 | App() 16 | } 17 | } -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Compose Multiplatform Application 2 | 3 | ## Before running! 4 | - install JDK 17 on your machine 5 | - add `local.properties` file to the project root and set a path to Android SDK there 6 | 7 | ### Android 8 | To run the application on android device/emulator: 9 | - open project in Android Studio and run imported android run configuration 10 | 11 | To build the application bundle: 12 | - run `./gradlew :composeApp:assembleDebug` 13 | - find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk` 14 | 15 | ### Desktop 16 | Run the desktop application: `./gradlew :composeApp:run` 17 | 18 | ### Browser 19 | Run the browser application: `./gradlew :composeApp:jsBrowserDevelopmentRun` 20 | 21 | -------------------------------------------------------------------------------- /sample/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | mavenLocal() 14 | } 15 | } 16 | 17 | includeBuild("../.") { 18 | dependencySubstitution { 19 | substitute(module("io.github.windedge.table:table")).using(project(":common")) 20 | substitute(module("io.github.windedge.table:table-m2")).using(project(":m2")) 21 | substitute(module("io.github.windedge.table:table-m3")).using(project(":m3")) 22 | } 23 | } 24 | 25 | rootProject.name = "Sample" 26 | include(":composeApp") 27 | 28 | -------------------------------------------------------------------------------- /m3/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(libs.plugins.kotlin.kmp.get().pluginId) 3 | id(libs.plugins.android.library.get().pluginId) 4 | id(libs.plugins.compose.get().pluginId) 5 | id(libs.plugins.compiler.compose.get().pluginId) 6 | id(libs.plugins.maven.publish.get().pluginId) 7 | } 8 | 9 | kotlin { 10 | sourceSets { 11 | val commonMain by getting { 12 | dependencies { 13 | implementation(libs.coroutine) 14 | 15 | api(project(":common")) 16 | implementation(compose.components.resources) 17 | implementation(compose.material3) 18 | } 19 | } 20 | 21 | val jvmMain by getting { 22 | dependencies { 23 | implementation(compose.desktop.common) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /m2/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(libs.plugins.kotlin.kmp.get().pluginId) 3 | id(libs.plugins.android.library.get().pluginId) 4 | id(libs.plugins.compose.get().pluginId) 5 | id(libs.plugins.compiler.compose.get().pluginId) 6 | id(libs.plugins.maven.publish.get().pluginId) 7 | } 8 | 9 | kotlin { 10 | sourceSets { 11 | val commonMain by getting { 12 | dependencies { 13 | implementation(libs.coroutine) 14 | 15 | api(project(":common")) 16 | implementation(compose.components.resources) 17 | implementation(compose.material) 18 | } 19 | } 20 | 21 | val jvmMain by getting { 22 | dependencies { 23 | implementation(compose.desktop.common) 24 | } 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store 43 | 44 | .idea/ 45 | xcuserdata/ 46 | Pods/ 47 | 48 | *.jks 49 | *yarn.lock 50 | *local.properties 51 | 52 | .kotlin 53 | .aider* 54 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.21" 3 | coroutine = "1.8.0" 4 | compose = "1.8.0" 5 | buildconfig = "4.1.1" 6 | android-library = "8.0.2" 7 | maven-publish = "0.34.0" 8 | 9 | [libraries] 10 | coroutine = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutine" } 11 | 12 | [plugins] 13 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } 14 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 15 | kotlin-kmp = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 16 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 17 | compiler-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 18 | buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig" } 19 | android-library = { id = "com.android.library", version.ref = "android-library" } 20 | -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(libs.plugins.kotlin.kmp.get().pluginId) 3 | id(libs.plugins.android.library.get().pluginId) 4 | id(libs.plugins.compose.get().pluginId) 5 | id(libs.plugins.compiler.compose.get().pluginId) 6 | id(libs.plugins.maven.publish.get().pluginId) 7 | } 8 | 9 | kotlin { 10 | sourceSets { 11 | val commonMain by getting { 12 | dependencies { 13 | implementation(libs.coroutine) 14 | 15 | implementation(compose.runtime) 16 | implementation(compose.foundation) 17 | implementation(compose.components.resources) 18 | } 19 | } 20 | 21 | val jvmMain by getting { 22 | dependencies { 23 | implementation(compose.desktop.common) 24 | } 25 | } 26 | } 27 | } 28 | 29 | compose.resources { 30 | packageOfResClass = "io.github.windedge.table.res" 31 | publicResClass = true 32 | } 33 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/compose/table/demo/theme/Theme.android.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo.theme 2 | 3 | import android.app.Activity 4 | import android.graphics.Color 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.ui.platform.LocalView 8 | import androidx.core.view.WindowCompat 9 | 10 | @Composable 11 | internal actual fun SystemAppearance(isDark: Boolean) { 12 | val view = LocalView.current 13 | val systemBarColor = Color.TRANSPARENT 14 | LaunchedEffect(isDark) { 15 | val window = (view.context as Activity).window 16 | WindowCompat.setDecorFitsSystemWindows(window, false) 17 | window.statusBarColor = systemBarColor 18 | window.navigationBarColor = systemBarColor 19 | WindowCompat.getInsetsController(window, window.decorView).apply { 20 | isAppearanceLightStatusBars = isDark 21 | isAppearanceLightNavigationBars = isDark 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/io/github/windedge/table/components/Divider.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.platform.LocalDensity 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun Divider( 16 | modifier: Modifier = Modifier, 17 | thickness: Dp = 1.dp, 18 | color: Color = Color.LightGray 19 | ) { 20 | val targetThickness = if (thickness == Dp.Hairline) { 21 | (1f / LocalDensity.current.density).dp 22 | } else { 23 | thickness 24 | } 25 | Box( 26 | modifier 27 | .fillMaxWidth() 28 | .height(targetThickness) 29 | .background(color = color) 30 | ) 31 | } -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/io/github/windedge/table/TableDslBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | 8 | @DslMarker 9 | annotation class TableDslBuilder 10 | 11 | @TableDslBuilder 12 | interface ColumnBuilder { 13 | fun column( 14 | modifier: Modifier = Modifier, 15 | contentAlignment: Alignment = Alignment.CenterStart, 16 | composable: @Composable() (BoxScope.() -> Unit) 17 | ) 18 | 19 | fun headerBackground(composable: @Composable () -> Unit) 20 | } 21 | 22 | @TableDslBuilder 23 | interface RowsBuilder { 24 | fun row(modifier: Modifier = Modifier, content: RowBuilderImpl.() -> Unit) 25 | } 26 | 27 | @TableDslBuilder 28 | interface RowBuilder { 29 | fun cell( 30 | modifier: Modifier = Modifier, 31 | contentAlignment: Alignment = Alignment.CenterStart, 32 | content: @Composable() (BoxScope.() -> Unit) 33 | ) 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /sample/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlin = "2.1.21" 4 | compose = "1.8.0" 5 | agp = "8.0.2" 6 | androidx-appcompat = "1.6.1" 7 | androidx-activityCompose = "1.8.1" 8 | compose-uitooling = "1.8.1" 9 | compose-icons-core = "1.7.3" 10 | 11 | [libraries] 12 | 13 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 14 | androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 15 | compose-uitooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-uitooling" } 16 | compose-material-icons-core = {module = "org.jetbrains.compose.material:material-icons-core", version.ref="compose-icons-core"} 17 | 18 | [plugins] 19 | 20 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 21 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 22 | compiler-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 23 | android-application = { id = "com.android.application", version.ref = "agp" } 24 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/compose/table/demo/App.android.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | 10 | class AndroidApp : Application() { 11 | companion object { 12 | lateinit var INSTANCE: AndroidApp 13 | } 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | INSTANCE = this 18 | } 19 | } 20 | 21 | class AppActivity : ComponentActivity() { 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | App() 26 | } 27 | } 28 | } 29 | 30 | internal actual fun openUrl(url: String?) { 31 | val uri = url?.let { Uri.parse(it) } ?: return 32 | val intent = Intent().apply { 33 | action = Intent.ACTION_VIEW 34 | data = uri 35 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 36 | } 37 | AndroidApp.INSTANCE.startActivity(intent) 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 windedge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.daemon.jvmargs=-Xmx4096M 3 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError 4 | org.gradle.caching=true 5 | # org.gradle.configuration-cache=true 6 | 7 | ## Turns on Kotlin 2.0 in this project 8 | #kotlin.experimental.tryK2=true 9 | 10 | # Makes sure we can debug our Kotlin plugin with breakpoints 11 | kotlin.compiler.execution.strategy=in-process 12 | 13 | #android.disableAutomaticComponentCreation=true 14 | android.useAndroidX=true 15 | 16 | #Compose 17 | org.jetbrains.compose.experimental.uikit.enabled=true 18 | org.jetbrains.compose.experimental.jscanvas.enabled=true 19 | 20 | 21 | GROUP=io.github.windedge.table 22 | VERSION_NAME=0.2.3 23 | 24 | mavenCentralPublishing=true 25 | signAllPublications=true 26 | 27 | POM_DESCRIPTION=Construct an object with copy and map 28 | POM_URL=https://github.com/windedge/compose-table 29 | 30 | POM_LICENSE_NAME=MIT License 31 | POM_LICENSE_URL=https://github.com/windedge/compose-table/blob/main/LICENSE 32 | POM_LICENSE_DIST=repo 33 | 34 | POM_SCM_URL=https://github.com/windedge/compose-table/tree/main 35 | POM_SCM_CONNECTION=scm:git:github.com/windedge/compose-table.git 36 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/windedge/compose-table.git 37 | 38 | POM_DEVELOPER_ID=windedge 39 | POM_DEVELOPER_NAME=JL Xu 40 | POM_DEVELOPER_EMAIL=windedge99@gmail.com 41 | -------------------------------------------------------------------------------- /m3/src/commonMain/kotlin/io/github/windedge/table/m3/PaginatedDataTable.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table.m3 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import io.github.windedge.table.BasicPaginatedDataTable 10 | import io.github.windedge.table.ColumnBuilder 11 | import io.github.windedge.table.PaginationRowBuilder 12 | import io.github.windedge.table.PaginationState 13 | import io.github.windedge.table.components.Divider 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlin.coroutines.CoroutineContext 16 | 17 | @Composable 18 | fun PaginatedDataTable( 19 | columns: ColumnBuilder.() -> Unit, 20 | paginationState: PaginationState, 21 | onPageChanged: suspend (PaginationState) -> List, 22 | context: CoroutineContext = Dispatchers.Default, 23 | modifier: Modifier = Modifier, 24 | cellPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 5.dp), 25 | divider: @Composable ((rowIndex: Int) -> Unit)? = @Composable { Divider() }, 26 | footer: @Composable BoxScope.() -> Unit = { 27 | Paginator(paginationState, modifier = Modifier.align(Alignment.CenterEnd)) 28 | }, 29 | eachRow: PaginationRowBuilder.(T) -> Unit 30 | ) { 31 | BasicPaginatedDataTable(columns, paginationState, onPageChanged, context, modifier, cellPadding, divider, footer, eachRow) 32 | } 33 | -------------------------------------------------------------------------------- /m2/src/commonMain/kotlin/io/github/windedge/table/m2/PaginatedDataTable.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table.m2 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import io.github.windedge.table.BasicPaginatedDataTable 10 | import io.github.windedge.table.ColumnBuilder 11 | import io.github.windedge.table.PaginationRowBuilder 12 | import io.github.windedge.table.PaginationState 13 | import io.github.windedge.table.components.Divider 14 | import io.github.windedge.table.material.Paginator 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlin.coroutines.CoroutineContext 17 | 18 | @Composable 19 | fun PaginatedDataTable( 20 | columns: ColumnBuilder.() -> Unit, 21 | paginationState: PaginationState, 22 | onPageChanged: suspend (PaginationState) -> List, 23 | context: CoroutineContext = Dispatchers.Default, 24 | modifier: Modifier = Modifier, 25 | cellPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 5.dp), 26 | divider: @Composable ((rowIndex: Int) -> Unit)? = @Composable { Divider() }, 27 | footer: @Composable BoxScope.() -> Unit = { 28 | Paginator(paginationState, modifier = Modifier.align(Alignment.CenterEnd)) 29 | }, 30 | eachRow: PaginationRowBuilder.(T) -> Unit 31 | ) { 32 | BasicPaginatedDataTable(columns, paginationState, onPageChanged, context, modifier, cellPadding, divider, footer, eachRow) 33 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | 3 | on: 4 | workflow_dispatch: # Manual trigger 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | publish: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Setup Xcode 24 | uses: maxim-lobanov/setup-xcode@v1 25 | with: 26 | xcode-version: latest-stable 27 | 28 | - name: Grant execute permission for gradlew 29 | run: chmod +x gradlew 30 | 31 | - name: Setup Android SDK 32 | uses: android-actions/setup-android@v2 33 | 34 | - name: Setup Gradle 35 | uses: gradle/gradle-build-action@v2 36 | 37 | - name: Import GPG key 38 | run: | 39 | # Create GPG directory 40 | mkdir -p ~/.gnupg/ 41 | chmod 700 ~/.gnupg/ 42 | 43 | # Setup GPG on macOS 44 | echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf 45 | echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf 46 | 47 | # Import key 48 | echo "${{ secrets.GPG_KEY_CONTENTS }}" | base64 --decode | gpg --batch --import 49 | gpg --list-secret-keys --keyid-format LONG 50 | env: 51 | GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} 52 | 53 | - name: Publish to Maven Central 54 | env: 55 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} 56 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} 57 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }} 58 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} 59 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 60 | run: | 61 | ./gradlew publishToMavenCentral --no-daemon --no-parallel 62 | -------------------------------------------------------------------------------- /m2/src/commonMain/kotlin/io/github/windedge/table/m2/Paginator.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table.material 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.material.Icon 6 | import androidx.compose.material.IconButton 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import io.github.windedge.table.PaginationState 12 | import io.github.windedge.table.res.* 13 | import org.jetbrains.compose.resources.ExperimentalResourceApi 14 | import org.jetbrains.compose.resources.painterResource 15 | 16 | @OptIn(ExperimentalResourceApi::class) 17 | @Composable 18 | fun Paginator( 19 | paginationState: PaginationState, 20 | modifier: Modifier = Modifier, 21 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 22 | verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, 23 | ) { 24 | Row(modifier, horizontalArrangement, verticalAlignment) { 25 | val pageIndex = paginationState.pageIndex 26 | val pageCount = paginationState.pageCount 27 | val totalCount = paginationState.totalCount 28 | val pageSize = paginationState.pageSize 29 | 30 | val start = (pageIndex - 1) * pageSize + 1 31 | val end = (start + pageSize - 1).coerceAtMost(totalCount) 32 | 33 | Text("$start-$end of $totalCount") 34 | IconButton(onClick = { paginationState.goto(1) }, enabled = pageIndex > 1) { 35 | Icon(painterResource(Res.drawable.skip_left), "First") 36 | } 37 | IconButton(onClick = { paginationState.previous() }, enabled = pageIndex > 1) { 38 | Icon(painterResource(Res.drawable.arrow_left), "Previous") 39 | } 40 | IconButton(onClick = { paginationState.next() }, enabled = pageIndex < pageCount) { 41 | Icon(painterResource(Res.drawable.arrow_right), "Next") 42 | } 43 | IconButton(onClick = { paginationState.goto(pageCount) }, enabled = pageIndex < pageCount) { 44 | Icon(painterResource(Res.drawable.skip_right), "Last") 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /m3/src/commonMain/kotlin/io/github/windedge/table/m3/Paginator.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import io.github.windedge.table.PaginationState 12 | import io.github.windedge.table.res.Res 13 | import io.github.windedge.table.res.* 14 | import org.jetbrains.compose.resources.ExperimentalResourceApi 15 | import org.jetbrains.compose.resources.painterResource 16 | import kotlin.math.min 17 | 18 | @OptIn(ExperimentalResourceApi::class) 19 | @Composable 20 | fun Paginator( 21 | paginationState: PaginationState, 22 | modifier: Modifier = Modifier, 23 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 24 | verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, 25 | ) { 26 | Row(modifier, horizontalArrangement, verticalAlignment) { 27 | val pageIndex = paginationState.pageIndex 28 | val pageCount = paginationState.pageCount 29 | val totalCount = paginationState.totalCount 30 | val pageSize = paginationState.pageSize 31 | 32 | val start = min((pageIndex - 1) * pageSize + 1, totalCount) 33 | val end = (start + pageSize - 1).coerceAtMost(totalCount) 34 | 35 | Text("$start-$end of $totalCount") 36 | IconButton(onClick = { paginationState.goto(1) }, enabled = pageIndex > 1) { 37 | Icon(painterResource(Res.drawable.skip_left), "First") 38 | } 39 | IconButton(onClick = { paginationState.previous() }, enabled = pageIndex > 1) { 40 | Icon(painterResource(Res.drawable.arrow_left), "Previous") 41 | } 42 | IconButton(onClick = { paginationState.next() }, enabled = pageIndex < pageCount) { 43 | Icon(painterResource(Res.drawable.arrow_right), "Next") 44 | } 45 | IconButton(onClick = { paginationState.goto(pageCount) }, enabled = pageIndex < pageCount) { 46 | Icon(painterResource(Res.drawable.skip_right), "Last") 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /m2/src/jvmMain/kotlin/Preview.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.desktop.ui.tooling.preview.Preview 2 | import androidx.compose.foundation.background 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import io.github.windedge.table.DataTable 9 | import io.github.windedge.table.m2.PaginatedDataTable 10 | import io.github.windedge.table.rememberPaginationState 11 | 12 | @Preview 13 | @Composable 14 | fun DataTablePreview() { 15 | val data = listOf( 16 | mapOf("Name" to "John Doe", "Age" to "30", "Email" to "john.doe@example.com"), 17 | mapOf("Name" to "Jane Doe", "Age" to "25", "Email" to "jane.doe@example.com"), 18 | ) 19 | 20 | DataTable( 21 | columns = { 22 | headerBackground { 23 | Box(modifier = Modifier.background(color = Color.LightGray)) 24 | } 25 | column { Text("Name") } 26 | column { Text("Age") } 27 | column { Text("Email") } 28 | }, 29 | ) { 30 | data.forEach { row -> 31 | row(modifier = Modifier) { 32 | cell { Text(row["Name"] ?: "") } 33 | cell { Text(row["Age"] ?: "") } 34 | cell { Text(row["Email"] ?: "") } 35 | } 36 | } 37 | } 38 | 39 | } 40 | 41 | @Preview 42 | @Composable 43 | fun PaginatedDataTablePreview() { 44 | val data = List(50) { 45 | mapOf("Column 1" to "Item $it", "Column 2" to "Item $it", "Column 3" to "Item $it") 46 | } 47 | val paginationState = rememberPaginationState(data.size, pageSize = 5) 48 | 49 | PaginatedDataTable( 50 | columns = { 51 | headerBackground { 52 | Box(Modifier.background(Color.LightGray)) 53 | } 54 | 55 | column { Text("Column 1") } 56 | column { Text("Column 2") } 57 | column { Text("Column 3") } 58 | }, 59 | paginationState = paginationState, 60 | onPageChanged = { 61 | data.chunked(it.pageSize)[it.pageIndex] 62 | } 63 | ) { item: Map -> 64 | row(modifier = Modifier) { 65 | cell { Text(item["Column 1"] ?: "") } 66 | cell { Text(item["Column 2"] ?: "") } 67 | cell { Text(item["Column 3"] ?: "") } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /m3/src/jvmMain/kotlin/Preview.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.desktop.ui.tooling.preview.Preview 2 | import androidx.compose.foundation.background 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import io.github.windedge.table.DataTable 9 | import io.github.windedge.table.m3.PaginatedDataTable 10 | import io.github.windedge.table.rememberPaginationState 11 | 12 | @Preview 13 | @Composable 14 | fun DataTablePreview() { 15 | val data = listOf( 16 | mapOf("Name" to "John Doe", "Age" to "30", "Email" to "john.doe@example.com"), 17 | mapOf("Name" to "Jane Doe", "Age" to "25", "Email" to "jane.doe@example.com"), 18 | ) 19 | 20 | DataTable( 21 | columns = { 22 | headerBackground { 23 | Box(modifier = Modifier.background(color = Color.LightGray)) 24 | } 25 | column { Text("Name") } 26 | column { Text("Age") } 27 | column { Text("Email") } 28 | }, 29 | ) { 30 | data.forEach { row -> 31 | row(modifier = Modifier) { 32 | cell { Text(row["Name"] ?: "") } 33 | cell { Text(row["Age"] ?: "") } 34 | cell { Text(row["Email"] ?: "") } 35 | } 36 | } 37 | } 38 | 39 | } 40 | 41 | @Preview 42 | @Composable 43 | fun PaginatedDataTablePreview() { 44 | val data = List(50) { 45 | mapOf("Column 1" to "Item $it", "Column 2" to "Item $it", "Column 3" to "Item $it") 46 | } 47 | val paginationState = rememberPaginationState(data.size, pageSize = 5) 48 | 49 | PaginatedDataTable( 50 | columns = { 51 | headerBackground { 52 | Box(Modifier.background(Color.LightGray)) 53 | } 54 | 55 | column { Text("Column 1") } 56 | column { Text("Column 2") } 57 | column { Text("Column 3") } 58 | }, 59 | paginationState = paginationState, 60 | onPageChanged = { 61 | data.chunked(it.pageSize)[it.pageIndex] 62 | } 63 | ) { item: Map -> 64 | row(modifier = Modifier) { 65 | cell { Text(item["Column 1"] ?: "") } 66 | cell { Text(item["Column 2"] ?: "") } 67 | cell { Text(item["Column 3"] ?: "") } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Tags starting with 'v' will trigger this workflow 7 | 8 | jobs: 9 | release: 10 | runs-on: macos-latest 11 | permissions: 12 | contents: write # This allows the workflow to create releases 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 # Fetch full history to support version calculation 18 | 19 | - name: Get tag version 20 | id: get_version 21 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 22 | 23 | - name: Update version in gradle.properties 24 | run: | 25 | # Ensure we have the latest code 26 | git fetch origin 27 | git checkout main 28 | 29 | # Update version number 30 | sed -i '' "s/VERSION_NAME=.*/VERSION_NAME=${{ steps.get_version.outputs.VERSION }}/" gradle.properties 31 | 32 | # Commit and push changes 33 | git config --local user.email "action@github.com" 34 | git config --local user.name "GitHub Action" 35 | git commit -m "Update version to ${{ steps.get_version.outputs.VERSION }}" -a || echo "No changes to commit" 36 | git push origin HEAD:main 37 | 38 | - name: Create Release 39 | uses: softprops/action-gh-release@v2 40 | with: 41 | name: Release ${{ steps.get_version.outputs.VERSION }} 42 | body: | 43 | Release version ${{ steps.get_version.outputs.VERSION }} 44 | draft: false 45 | prerelease: false 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }} # Use a personal access token instead of the default token 48 | 49 | # - name: Update to next development version 50 | # run: | 51 | # # Calculate next version number 52 | # CURRENT_VERSION="${{ steps.get_version.outputs.VERSION }}" 53 | # # Split version number 54 | # IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" 55 | # PATCH=$((VERSION_PARTS[2] + 1)) 56 | # NEXT_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.$PATCH-SNAPSHOT" 57 | 58 | # # Update to next development version 59 | # sed -i '' "s/VERSION_NAME=.*/VERSION_NAME=$NEXT_VERSION/" gradle.properties 60 | 61 | # # Commit and push changes 62 | # git config --local user.email "action@github.com" 63 | # git config --local user.name "GitHub Action" 64 | # git commit -m "Prepare for next development version $NEXT_VERSION" -a || echo "No changes to commit" 65 | # git push origin HEAD:main 66 | -------------------------------------------------------------------------------- /.github/GUIDE.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow Explanation 2 | 3 | This project uses GitHub Actions to automate publishing to Maven Central. The project uses the `com.vanniktech.maven.publish` plugin to handle the Maven Central publishing process. 4 | 5 | ## Configuring Secrets 6 | 7 | Before using the workflow, ensure that the following secrets are added to the GitHub repository settings: 8 | 9 | - `OSSRH_USERNAME`: Maven Central (Sonatype OSSRH) username 10 | - `OSSRH_PASSWORD`: Maven Central (Sonatype OSSRH) password 11 | - `SIGNING_KEY_ID`: GPG signing key ID (last 8 digits) 12 | - `SIGNING_PASSWORD`: GPG signing key password 13 | - `GPG_KEY_CONTENTS`: Base64-encoded GPG private key (use the command: `gpg --export-secret-keys YOUR_KEY_ID | base64 -w 0`) 14 | - `SONATYPE_STAGING_PROFILE_ID`: Sonatype staging profile ID (optional) 15 | 16 | These environment variables are mapped to the corresponding Gradle properties as required by the `com.vanniktech.maven.publish` plugin. 17 | 18 | ## Publishing Environment 19 | 20 | The publishing workflow for this project runs on the `macOS-latest` runner to support the iOS (iosArm64) target platform. The workflow automatically sets up: 21 | 22 | - JDK 17 23 | - The latest stable version of Xcode 24 | - Android SDK 25 | - Necessary build and signing tools 26 | - GPG tools for build signing 27 | 28 | ## Publishing Process 29 | 30 | ### Automatic Publishing (Based on Tags) 31 | 32 | 1. Create a new version tag, e.g., `v0.1.9`: 33 | ``` 34 | git tag -a v0.1.9 -m "Release v0.1.9" 35 | git push origin v0.1.9 36 | ``` 37 | 38 | 2. After pushing the tag, the `release.yml` workflow will be automatically triggered, which will: 39 | - Extract the version number from the tag 40 | - Update the version in `gradle.properties` 41 | - Create a GitHub Release 42 | 43 | 3. After creating the Release, the `publish.yml` workflow will be automatically triggered, which will: 44 | - Build the project (including the iOS platform) 45 | - Sign the build artifacts using GPG 46 | - Publish all artifacts to Maven Central using the `com.vanniktech.maven.publish` plugin 0.34.0 via the `publishToMavenCentral` task 47 | 48 | ### Manual Publishing 49 | 50 | 1. On the GitHub repository page, go to the "Actions" tab 51 | 2. Select the "Publish to Maven Central" workflow 52 | 3. Click the "Run workflow" button 53 | 4. Select the branch to publish from the dropdown menu 54 | 5. Click the "Run workflow" button to start the publishing process 55 | 56 | ## Notes 57 | 58 | - Ensure that the version number in `gradle.properties` is correctly formatted (if it's a snapshot version, it should end with `-SNAPSHOT`) 59 | - Test your library before publishing to ensure quality 60 | - Check the Maven Central Portal to verify that the publishing was successful 61 | - Building for the iOS platform requires a macOS environment and Xcode tools, which is why we use the macOS runner in GitHub Actions 62 | - The `com.vanniktech.maven.publish` plugin automatically handles most of the Maven Central publishing process, including signing and uploading 63 | -------------------------------------------------------------------------------- /sample/gradlew.bat: -------------------------------------------------------------------------------- 1 | 2 | @rem 3 | @rem Copyright 2015 the original author or authors. 4 | @rem 5 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 6 | @rem you may not use this file except in compliance with the License. 7 | @rem You may obtain a copy of the License at 8 | @rem 9 | @rem https://www.apache.org/licenses/LICENSE-2.0 10 | @rem 11 | @rem Unless required by applicable law or agreed to in writing, software 12 | @rem distributed under the License is distributed on an "AS IS" BASIS, 13 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | @rem See the License for the specific language governing permissions and 15 | @rem limitations under the License. 16 | @rem 17 | 18 | @if "%DEBUG%" == "" @echo off 19 | @rem ########################################################################## 20 | @rem 21 | @rem Gradle startup script for Windows 22 | @rem 23 | @rem ########################################################################## 24 | 25 | @rem Set local scope for the variables with windows NT shell 26 | if "%OS%"=="Windows_NT" setlocal 27 | 28 | set DIRNAME=%~dp0 29 | if "%DIRNAME%" == "" set DIRNAME=. 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if "%ERRORLEVEL%" == "0" goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compose Table 2 | 3 | This is a `Compose Multiplatform` table library that supports both Material and Material3 designs. 4 | 5 | 6 | ![screen](./screenshot.png) 7 | 8 | ## Supported Platforms 9 | 10 | * Android 11 | * Desktop 12 | * iOS 13 | * WasmJS 14 | * JS 15 | 16 | ## Usage 17 | 18 | ### Setup 19 | 20 | Add the MavenCentral repository to your build file 21 | ```kotlin 22 | repositories { 23 | ... 24 | mavenCentral() 25 | } 26 | ``` 27 | 28 | Add dependency 29 | 30 | ```kotlin 31 | implementation("io.github.windedge.table:table-m2:") 32 | // or 33 | implementation("io.github.windedge.table:table-m3:") 34 | ``` 35 | 36 | ### Static Data Table 37 | 38 | ```kotlin 39 | import androidx.compose.material3.Text 40 | import io.github.windedge.table.DataTable 41 | 42 | val data = listOf( 43 | mapOf("Name" to "John Doe", "Age" to "30", "Email" to "john.doe@example.com"), 44 | mapOf("Name" to "Jane Doe", "Age" to "25", "Email" to "jane.doe@example.com") 45 | ) 46 | 47 | DataTable( 48 | columns = { 49 | headerBackground { 50 | Box(modifier = Modifier.background(color = Color.LightGray)) 51 | } 52 | column { Text("Name") } 53 | column { Text("Age") } 54 | column { Text("Email") } 55 | } 56 | ) { 57 | data.forEach { record -> 58 | row(modifier = Modifier) { 59 | cell { Text(record["Name"] ?: "") } 60 | cell { Text(record["Age"] ?: "") } 61 | cell { Text(record["Email"] ?: "") } 62 | } 63 | } 64 | } 65 | 66 | ``` 67 | 68 | ### Paginated Data Table 69 | 70 | ```kotlin 71 | import androidx.compose.material3.Text 72 | import io.github.windedge.table.m3.PaginatedDataTable 73 | import io.github.windedge.table.rememberPaginationState 74 | 75 | val data = List(50) { 76 | mapOf("Column 1" to "Item $it", "Column 2" to "Item $it", "Column 3" to "Item $it") 77 | } 78 | val paginationState = rememberPaginationState(data.size, pageSize = 5) 79 | 80 | PaginatedDataTable( 81 | columns = { 82 | headerBackground { 83 | Box(Modifier.background(colorScheme.primary)) 84 | } 85 | column { Text("Column 1") } 86 | column { Text("Column 2") } 87 | column { Text("Column 3") } 88 | }, 89 | paginationState = paginationState, 90 | onPageChanged = { 91 | data.chunked(it.pageSize)[it.pageIndex - 1] 92 | } 93 | ) { item: Map -> 94 | row(modifier = Modifier) { 95 | cell { Text(item["Column 1"] ?: "") } 96 | cell { Text(item["Column 2"] ?: "") } 97 | cell { Text(item["Column 3"] ?: "") } 98 | } 99 | } 100 | ``` 101 | 102 | Please check the [sample app](./sample) for a more detailed showcase. 103 | 104 | ## Known Issues 105 | 106 | Since the Compose Resources is relatively new and still under development, a bug in the WASM platform is causing the paginator icons to fail to load as expected. 107 | 108 | ## Credits 109 | 110 | This project is inspired by [compose-data-table](https://github.com/sproctor/compose-data-table). 111 | 112 | 113 | ## License 114 | 115 | This project is licensed under the MIT License. 116 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/compose/table/demo/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | //generated by https://m3.material.io/theme-builder#/custom 6 | //Color palette was taken here: https://colorhunt.co/palettes/popular 7 | 8 | internal val md_theme_light_primary = Color(0xFF00687A) 9 | internal val md_theme_light_onPrimary = Color(0xFFFFFFFF) 10 | internal val md_theme_light_primaryContainer = Color(0xFFABEDFF) 11 | internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26) 12 | internal val md_theme_light_secondary = Color(0xFF00696E) 13 | internal val md_theme_light_onSecondary = Color(0xFFFFFFFF) 14 | internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE) 15 | internal val md_theme_light_onSecondaryContainer = Color(0xFF002022) 16 | internal val md_theme_light_tertiary = Color(0xFF904D00) 17 | internal val md_theme_light_onTertiary = Color(0xFFFFFFFF) 18 | internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2) 19 | internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500) 20 | internal val md_theme_light_error = Color(0xFFBA1A1A) 21 | internal val md_theme_light_errorContainer = Color(0xFFFFDAD6) 22 | internal val md_theme_light_onError = Color(0xFFFFFFFF) 23 | internal val md_theme_light_onErrorContainer = Color(0xFF410002) 24 | internal val md_theme_light_background = Color(0xFFFFFBFF) 25 | internal val md_theme_light_onBackground = Color(0xFF221B00) 26 | internal val md_theme_light_surface = Color(0xFFFFFBFF) 27 | internal val md_theme_light_onSurface = Color(0xFF221B00) 28 | internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7) 29 | internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B) 30 | internal val md_theme_light_outline = Color(0xFF70797B) 31 | internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0) 32 | internal val md_theme_light_inverseSurface = Color(0xFF3A3000) 33 | internal val md_theme_light_inversePrimary = Color(0xFF55D6F4) 34 | internal val md_theme_light_shadow = Color(0xFF000000) 35 | internal val md_theme_light_surfaceTint = Color(0xFF00687A) 36 | internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB) 37 | internal val md_theme_light_scrim = Color(0xFF000000) 38 | 39 | internal val md_theme_dark_primary = Color(0xFF55D6F4) 40 | internal val md_theme_dark_onPrimary = Color(0xFF003640) 41 | internal val md_theme_dark_primaryContainer = Color(0xFF004E5C) 42 | internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF) 43 | internal val md_theme_dark_secondary = Color(0xFF4CD9E2) 44 | internal val md_theme_dark_onSecondary = Color(0xFF00373A) 45 | internal val md_theme_dark_secondaryContainer = Color(0xFF004F53) 46 | internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE) 47 | internal val md_theme_dark_tertiary = Color(0xFFFFB77C) 48 | internal val md_theme_dark_onTertiary = Color(0xFF4D2700) 49 | internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900) 50 | internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2) 51 | internal val md_theme_dark_error = Color(0xFFFFB4AB) 52 | internal val md_theme_dark_errorContainer = Color(0xFF93000A) 53 | internal val md_theme_dark_onError = Color(0xFF690005) 54 | internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 55 | internal val md_theme_dark_background = Color(0xFF221B00) 56 | internal val md_theme_dark_onBackground = Color(0xFFFFE264) 57 | internal val md_theme_dark_surface = Color(0xFF221B00) 58 | internal val md_theme_dark_onSurface = Color(0xFFFFE264) 59 | internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B) 60 | internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB) 61 | internal val md_theme_dark_outline = Color(0xFF899295) 62 | internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00) 63 | internal val md_theme_dark_inverseSurface = Color(0xFFFFE264) 64 | internal val md_theme_dark_inversePrimary = Color(0xFF00687A) 65 | internal val md_theme_dark_shadow = Color(0xFF000000) 66 | internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4) 67 | internal val md_theme_dark_outlineVariant = Color(0xFF3F484B) 68 | internal val md_theme_dark_scrim = Color(0xFF000000) 69 | 70 | 71 | internal val seed = Color(0xFF2C3639) 72 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/io/github/windedge/table/BasicPaginatedDataTable.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.runtime.* 6 | import androidx.compose.runtime.saveable.Saver 7 | import androidx.compose.runtime.saveable.listSaver 8 | import androidx.compose.ui.unit.dp 9 | import androidx.compose.runtime.saveable.rememberSaveable 10 | import androidx.compose.ui.Modifier 11 | import io.github.windedge.table.components.Divider 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.launch 14 | import kotlin.coroutines.CoroutineContext 15 | 16 | 17 | @Composable 18 | fun BasicPaginatedDataTable( 19 | columns: ColumnBuilder.() -> Unit, 20 | paginationState: PaginationState, 21 | onPageChanged: suspend (PaginationState) -> List, 22 | context: CoroutineContext = Dispatchers.Default, 23 | modifier: Modifier = Modifier, 24 | cellPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 5.dp), 25 | divider: @Composable ((rowIndex: Int) -> Unit)? = @Composable { Divider() }, 26 | footer: @Composable (BoxScope.() -> Unit)? = null, 27 | eachRow: PaginationRowBuilder.(T) -> Unit 28 | ) { 29 | val recordList = remember { mutableStateListOf() } 30 | LaunchedEffect(paginationState.totalCount, paginationState.pageSize, paginationState.pageIndex) { 31 | launch(context) { 32 | onPageChanged(paginationState).let { 33 | recordList.clear() 34 | recordList.addAll(it) 35 | } 36 | } 37 | } 38 | 39 | DataTable(columns, modifier, cellPadding, divider, footer) { 40 | recordList.forEachIndexed { index, record -> 41 | PaginationRowBuilderImpl(index, this).apply { eachRow(record) } 42 | } 43 | } 44 | } 45 | 46 | @TableDslBuilder 47 | interface PaginationRowBuilder { 48 | val rowIndex: Int 49 | fun row(modifier: Modifier = Modifier, content: RowBuilder.() -> Unit) 50 | } 51 | 52 | @Suppress("unused") 53 | class PaginationRowBuilderImpl(override val rowIndex: Int, private val parent: RowsBuilder) : PaginationRowBuilder { 54 | override fun row(modifier: Modifier, content: RowBuilder.() -> Unit) { 55 | parent.row(modifier) { content() } 56 | } 57 | } 58 | 59 | @Suppress("MemberVisibilityCanBePrivate", "unused") 60 | class PaginationState(totalCount: Int, pageIndex: Int, pageSize: Int) { 61 | var totalCount by mutableStateOf(totalCount) 62 | private set 63 | var pageSize by mutableStateOf(pageSize) 64 | private set 65 | var pageIndex by mutableStateOf(pageIndex) 66 | private set 67 | 68 | val pageCount: Int get() = (totalCount + pageSize - 1) / pageSize 69 | 70 | fun ensurePageCountPositive(): Int = if (pageCount < 1) 1 else pageCount 71 | 72 | fun next() { 73 | pageIndex = (pageIndex + 1).coerceIn(1, ensurePageCountPositive()) 74 | } 75 | 76 | fun previous() { 77 | pageIndex = (pageIndex - 1).coerceIn(1, ensurePageCountPositive()) 78 | } 79 | 80 | fun goto(index: Int) { 81 | pageIndex = index.coerceIn(1, ensurePageCountPositive()) 82 | } 83 | 84 | fun changeTotalCount(count: Int) { 85 | totalCount = count 86 | } 87 | 88 | fun changePageSize(size: Int) { 89 | pageSize = size 90 | } 91 | 92 | companion object { 93 | val Saver: Saver = listSaver( 94 | save = { listOf(it.totalCount, it.pageIndex, it.pageSize) }, 95 | restore = { 96 | PaginationState(it[0], it[1], it[2]) 97 | } 98 | ) 99 | } 100 | } 101 | 102 | @Composable 103 | fun rememberPaginationState( 104 | initialTotalCount: Int, 105 | initialPageIndex: Int = 1, 106 | pageSize: Int = 10, 107 | ): PaginationState { 108 | var pageIndex by remember { mutableStateOf(initialPageIndex) } 109 | val state = rememberSaveable(initialTotalCount, initialPageIndex, pageSize, saver = PaginationState.Saver) { 110 | PaginationState(initialTotalCount, pageIndex, pageSize).apply { goto(this.pageIndex) } 111 | } 112 | LaunchedEffect(state.pageIndex) { pageIndex = state.pageIndex } 113 | return state 114 | } 115 | -------------------------------------------------------------------------------- /sample/composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 4 | 5 | plugins { 6 | alias(libs.plugins.multiplatform) 7 | alias(libs.plugins.compose) 8 | alias(libs.plugins.compiler.compose) 9 | alias(libs.plugins.android.application) 10 | } 11 | 12 | kotlin { 13 | androidTarget { 14 | compilations.all { 15 | kotlinOptions { jvmTarget = "17" } 16 | } 17 | } 18 | 19 | jvm() 20 | 21 | listOf( 22 | iosX64(), 23 | iosArm64(), 24 | iosSimulatorArm64() 25 | ).forEach { iosTarget -> 26 | iosTarget.binaries.framework { 27 | baseName = "ComposeTable" 28 | isStatic = true 29 | } 30 | } 31 | 32 | @OptIn(ExperimentalWasmDsl::class) 33 | wasmJs { 34 | outputModuleName = "composeApp" 35 | browser { 36 | commonWebpackConfig { 37 | outputFileName = "composeApp.js" 38 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 39 | static = (static ?: mutableListOf()).apply { 40 | // Serve sources to debug inside browser 41 | add(project.rootDir.path) 42 | add(project.projectDir.path) 43 | } 44 | } 45 | } 46 | } 47 | binaries.executable() 48 | } 49 | 50 | js { 51 | browser { 52 | outputModuleName = "composeApp" 53 | commonWebpackConfig { 54 | showProgress = true 55 | } 56 | runTask { 57 | mainOutputFileName = "composeApp.js" 58 | } 59 | webpackTask { 60 | mainOutputFileName = "composeApp.js" 61 | } 62 | binaries.executable() 63 | } 64 | } 65 | 66 | sourceSets { 67 | all { 68 | languageSettings { 69 | optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") 70 | } 71 | } 72 | commonMain.dependencies { 73 | implementation(compose.runtime) 74 | implementation(compose.foundation) 75 | implementation(compose.material3) 76 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) 77 | implementation(compose.components.resources) 78 | implementation(libs.compose.material.icons.core) 79 | 80 | implementation("io.github.windedge.table:table:") 81 | implementation("io.github.windedge.table:table-m3:") 82 | } 83 | 84 | commonTest.dependencies { 85 | implementation(kotlin("test")) 86 | } 87 | 88 | androidMain.dependencies { 89 | implementation(libs.androidx.appcompat) 90 | implementation(libs.androidx.activityCompose) 91 | implementation(libs.compose.uitooling) 92 | } 93 | 94 | jvmMain.dependencies { 95 | implementation(compose.desktop.common) 96 | implementation(compose.desktop.currentOs) 97 | } 98 | 99 | iosMain.dependencies { 100 | } 101 | 102 | val wasmJsMain by getting 103 | } 104 | } 105 | 106 | android { 107 | namespace = "compose.table.demo" 108 | compileSdk = 34 109 | 110 | defaultConfig { 111 | minSdk = 24 112 | targetSdk = 34 113 | 114 | applicationId = "compose.table.demo.androidApp" 115 | versionCode = 1 116 | versionName = "1.0.0" 117 | } 118 | sourceSets["main"].apply { 119 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 120 | res.srcDirs("src/androidMain/resources") 121 | resources.srcDirs("src/commonMain/resources") 122 | } 123 | compileOptions { 124 | sourceCompatibility = JavaVersion.VERSION_17 125 | targetCompatibility = JavaVersion.VERSION_17 126 | } 127 | } 128 | 129 | compose.desktop { 130 | application { 131 | mainClass = "MainKt" 132 | 133 | nativeDistributions { 134 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 135 | packageName = "compose.table.demo.desktopApp" 136 | packageVersion = "1.0.0" 137 | } 138 | } 139 | } 140 | 141 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/compose/table/demo/App.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo 2 | 3 | import androidx.compose.foundation.* 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.selection.toggleable 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.* 8 | import androidx.compose.material3.* 9 | import androidx.compose.material3.MaterialTheme.colorScheme 10 | import androidx.compose.material3.MaterialTheme.shapes 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.unit.dp 15 | import io.github.windedge.table.m3.PaginatedDataTable 16 | import io.github.windedge.table.rememberPaginationState 17 | 18 | data class User(val id: Int, val name: String, val email: String, val age: Int?) 19 | 20 | fun randomUser(id: Int): User { 21 | val name = arrayOf("Alice", "Bob", "Charlie").random() 22 | val email = arrayOf("charlie@example.com", "bob@example.com", "alice@example.com").random() 23 | val age = arrayOf(28, 30, null).random() 24 | 25 | return User(id, name, email, age) 26 | } 27 | 28 | @Composable 29 | fun App() { 30 | val vScrollState = rememberScrollState() 31 | Box(modifier = Modifier.padding(10.dp).verticalScroll(vScrollState)) { 32 | Column(modifier = Modifier) { 33 | val users = remember { 34 | mutableStateListOf().apply { 35 | repeat(18) { add(randomUser(it)) } 36 | } 37 | } 38 | 39 | val color = colorScheme.onPrimary 40 | val textStyle = MaterialTheme.typography.titleMedium 41 | val selectedIds = remember { mutableStateListOf() } 42 | val paginationState = rememberPaginationState(users.size, pageSize = 5) 43 | 44 | 45 | Button(onClick = { users.add(randomUser(users.size)) }) { 46 | Text("Add User") 47 | } 48 | 49 | PaginatedDataTable( 50 | columns = { 51 | headerBackground { 52 | Box(Modifier.clip(shapes.extraSmall).background(colorScheme.primary)) 53 | } 54 | 55 | column { 56 | val range = paginationState.run { 57 | (pageIndex - 1) * pageSize until pageIndex * pageSize 58 | } 59 | val checked = range.all { selectedIds.contains(it) } 60 | Checkbox( 61 | checked, 62 | colors = CheckboxDefaults.colors(colorScheme.inversePrimary, colorScheme.onPrimary), 63 | onCheckedChange = { 64 | if (it) range.forEach { selectedIds.add(it) } else range.forEach { selectedIds.remove(it) } 65 | } 66 | ) 67 | } 68 | 69 | column(modifier = Modifier.height(50.dp)) { 70 | Text( 71 | "Name", 72 | color = color, 73 | style = textStyle 74 | ) 75 | } 76 | column { Text("Email", color = color, style = textStyle) } 77 | column { Text("Age", color = color, style = textStyle) } 78 | column { } 79 | }, 80 | paginationState = paginationState, 81 | onPageChanged = { 82 | selectedIds.clear() 83 | users.chunked(it.pageSize)[(it.pageIndex - 1)] 84 | }, 85 | ) { user -> 86 | val checked = selectedIds.contains(user.id) 87 | row(modifier = Modifier.toggleable(checked, onValueChange = { 88 | if (it) selectedIds.add(user.id) else selectedIds.remove(user.id) 89 | })) { 90 | cell { 91 | Checkbox(checked, onCheckedChange = { 92 | if (it) selectedIds.add(user.id) else selectedIds.remove(user.id) 93 | }) 94 | } 95 | cell { Text(user.name) } 96 | cell { Text(user.email) } 97 | cell { Text(user.age?.toString() ?: "N/A") } 98 | cell { 99 | IconButton({ 100 | println("delete ${user.name}") 101 | }) { 102 | Icon(Icons.Filled.Delete, contentDescription = "Delete") 103 | } 104 | } 105 | } 106 | } 107 | 108 | } 109 | 110 | // val scrollbarAdapter = rememberScrollbarAdapter(scrollState) 111 | // VerticalScrollbar(scrollbarAdapter, modifier = Modifier.align(Alignment.CenterEnd)) 112 | } 113 | 114 | } 115 | 116 | 117 | internal expect fun openUrl(url: String?) -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/compose/table/demo/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package compose.table.demo.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material3.lightColorScheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Shapes 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.material3.Typography 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.CompositionLocalProvider 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.compositionLocalOf 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.font.FontFamily 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = md_theme_light_primary, 26 | onPrimary = md_theme_light_onPrimary, 27 | primaryContainer = md_theme_light_primaryContainer, 28 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 29 | secondary = md_theme_light_secondary, 30 | onSecondary = md_theme_light_onSecondary, 31 | secondaryContainer = md_theme_light_secondaryContainer, 32 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 33 | tertiary = md_theme_light_tertiary, 34 | onTertiary = md_theme_light_onTertiary, 35 | tertiaryContainer = md_theme_light_tertiaryContainer, 36 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 37 | error = md_theme_light_error, 38 | errorContainer = md_theme_light_errorContainer, 39 | onError = md_theme_light_onError, 40 | onErrorContainer = md_theme_light_onErrorContainer, 41 | background = md_theme_light_background, 42 | onBackground = md_theme_light_onBackground, 43 | surface = md_theme_light_surface, 44 | onSurface = md_theme_light_onSurface, 45 | surfaceVariant = md_theme_light_surfaceVariant, 46 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 47 | outline = md_theme_light_outline, 48 | inverseOnSurface = md_theme_light_inverseOnSurface, 49 | inverseSurface = md_theme_light_inverseSurface, 50 | inversePrimary = md_theme_light_inversePrimary, 51 | surfaceTint = md_theme_light_surfaceTint, 52 | outlineVariant = md_theme_light_outlineVariant, 53 | scrim = md_theme_light_scrim, 54 | ) 55 | 56 | private val DarkColorScheme = darkColorScheme( 57 | primary = md_theme_dark_primary, 58 | onPrimary = md_theme_dark_onPrimary, 59 | primaryContainer = md_theme_dark_primaryContainer, 60 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 61 | secondary = md_theme_dark_secondary, 62 | onSecondary = md_theme_dark_onSecondary, 63 | secondaryContainer = md_theme_dark_secondaryContainer, 64 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 65 | tertiary = md_theme_dark_tertiary, 66 | onTertiary = md_theme_dark_onTertiary, 67 | tertiaryContainer = md_theme_dark_tertiaryContainer, 68 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 69 | error = md_theme_dark_error, 70 | errorContainer = md_theme_dark_errorContainer, 71 | onError = md_theme_dark_onError, 72 | onErrorContainer = md_theme_dark_onErrorContainer, 73 | background = md_theme_dark_background, 74 | onBackground = md_theme_dark_onBackground, 75 | surface = md_theme_dark_surface, 76 | onSurface = md_theme_dark_onSurface, 77 | surfaceVariant = md_theme_dark_surfaceVariant, 78 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 79 | outline = md_theme_dark_outline, 80 | inverseOnSurface = md_theme_dark_inverseOnSurface, 81 | inverseSurface = md_theme_dark_inverseSurface, 82 | inversePrimary = md_theme_dark_inversePrimary, 83 | surfaceTint = md_theme_dark_surfaceTint, 84 | outlineVariant = md_theme_dark_outlineVariant, 85 | scrim = md_theme_dark_scrim, 86 | ) 87 | 88 | private val AppShapes = Shapes( 89 | extraSmall = RoundedCornerShape(2.dp), 90 | small = RoundedCornerShape(4.dp), 91 | medium = RoundedCornerShape(8.dp), 92 | large = RoundedCornerShape(16.dp), 93 | extraLarge = RoundedCornerShape(32.dp) 94 | ) 95 | 96 | private val AppTypography = Typography( 97 | bodyMedium = TextStyle( 98 | fontFamily = FontFamily.Default, 99 | fontWeight = FontWeight.Medium, 100 | fontSize = 16.sp 101 | ) 102 | ) 103 | 104 | internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } 105 | 106 | @Composable 107 | internal fun AppTheme( 108 | content: @Composable() () -> Unit 109 | ) { 110 | val systemIsDark = isSystemInDarkTheme() 111 | val isDarkState = remember { mutableStateOf(systemIsDark) } 112 | CompositionLocalProvider( 113 | LocalThemeIsDark provides isDarkState 114 | ) { 115 | val isDark by isDarkState 116 | SystemAppearance(!isDark) 117 | MaterialTheme( 118 | colorScheme = if (isDark) DarkColorScheme else LightColorScheme, 119 | typography = AppTypography, 120 | shapes = AppShapes, 121 | content = { 122 | Surface(content = content) 123 | } 124 | ) 125 | } 126 | } 127 | 128 | @Composable 129 | internal expect fun SystemAppearance(isDark: Boolean) 130 | -------------------------------------------------------------------------------- /sample/gradlew: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env sh 3 | 4 | # 5 | # Copyright 2015 the original author or authors. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | ############################################################################## 21 | ## 22 | ## Gradle start up script for UN*X 23 | ## 24 | ############################################################################## 25 | 26 | # Attempt to set APP_HOME 27 | # Resolve links: ${'$'}0 may be a link 28 | PRG="$0" 29 | # Need this for relative symlinks. 30 | while [ -h "$PRG" ] ; do 31 | ls=`ls -ld "$PRG"` 32 | link=`expr "$ls" : '.*-> \(.*\)${'$'}'` 33 | if expr "$link" : '/.*' > /dev/null; then 34 | PRG="$link" 35 | else 36 | PRG=`dirname "$PRG"`"/$link" 37 | fi 38 | done 39 | SAVED="`pwd`" 40 | cd "`dirname \"$PRG\"`/" >/dev/null 41 | APP_HOME="`pwd -P`" 42 | cd "$SAVED" >/dev/null 43 | 44 | APP_NAME="Gradle" 45 | APP_BASE_NAME=`basename "$0"` 46 | 47 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 48 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 49 | 50 | # Use the maximum available, or set MAX_FD != -1 to use that value. 51 | MAX_FD="maximum" 52 | 53 | warn () { 54 | echo "${'$'}*" 55 | } 56 | 57 | die () { 58 | echo 59 | echo "${'$'}*" 60 | echo 61 | exit 1 62 | } 63 | 64 | # OS specific support (must be 'true' or 'false'). 65 | cygwin=false 66 | msys=false 67 | darwin=false 68 | nonstop=false 69 | case "`uname`" in 70 | CYGWIN* ) 71 | cygwin=true 72 | ;; 73 | Darwin* ) 74 | darwin=true 75 | ;; 76 | MSYS* | MINGW* ) 77 | msys=true 78 | ;; 79 | NONSTOP* ) 80 | nonstop=true 81 | ;; 82 | esac 83 | 84 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 85 | 86 | 87 | # Determine the Java command to use to start the JVM. 88 | if [ -n "$JAVA_HOME" ] ; then 89 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 90 | # IBM's JDK on AIX uses strange locations for the executables 91 | JAVACMD="$JAVA_HOME/jre/sh/java" 92 | else 93 | JAVACMD="$JAVA_HOME/bin/java" 94 | fi 95 | if [ ! -x "$JAVACMD" ] ; then 96 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 97 | 98 | Please set the JAVA_HOME variable in your environment to match the 99 | location of your Java installation." 100 | fi 101 | else 102 | JAVACMD="java" 103 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 104 | 105 | Please set the JAVA_HOME variable in your environment to match the 106 | location of your Java installation." 107 | fi 108 | 109 | # Increase the maximum file descriptors if we can. 110 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 111 | MAX_FD_LIMIT=`ulimit -H -n` 112 | if [ $? -eq 0 ] ; then 113 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 114 | MAX_FD="$MAX_FD_LIMIT" 115 | fi 116 | ulimit -n $MAX_FD 117 | if [ $? -ne 0 ] ; then 118 | warn "Could not set maximum file descriptor limit: $MAX_FD" 119 | fi 120 | else 121 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 122 | fi 123 | fi 124 | 125 | # For Darwin, add options to specify how the application appears in the dock 126 | if $darwin; then 127 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 128 | fi 129 | 130 | # For Cygwin or MSYS, switch paths to Windows format before running java 131 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 132 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 133 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 134 | 135 | JAVACMD=`cygpath --unix "$JAVACMD"` 136 | 137 | # We build the pattern for arguments to be converted via cygpath 138 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 139 | SEP="" 140 | for dir in $ROOTDIRSRAW ; do 141 | ROOTDIRS="$ROOTDIRS$SEP$dir" 142 | SEP="|" 143 | done 144 | OURCYGPATTERN="(^($ROOTDIRS))" 145 | # Add a user-defined pattern to the cygpath arguments 146 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 147 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 148 | fi 149 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 150 | i=0 151 | for arg in "$@" ; do 152 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 153 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 154 | 155 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 156 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 157 | else 158 | eval `echo args$i`="\"$arg\"" 159 | fi 160 | i=`expr $i + 1` 161 | done 162 | case $i in 163 | 0) set -- ;; 164 | 1) set -- "$args0" ;; 165 | 2) set -- "$args0" "$args1" ;; 166 | 3) set -- "$args0" "$args1" "$args2" ;; 167 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 168 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 169 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 170 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 171 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 172 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 173 | esac 174 | fi 175 | 176 | # Escape application args 177 | save () { 178 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 179 | echo " " 180 | } 181 | APP_ARGS=`save "$@"` 182 | 183 | # Collect all arguments for the java command, following the shell quoting and substitution rules 184 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 185 | 186 | exec "$JAVACMD" "$@" 187 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/io/github/windedge/table/DataTable.kt: -------------------------------------------------------------------------------- 1 | package io.github.windedge.table 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.layout.Layout 8 | import androidx.compose.ui.layout.SubcomposeLayout 9 | import androidx.compose.ui.unit.Constraints 10 | import androidx.compose.ui.unit.dp 11 | import io.github.windedge.table.components.Divider 12 | import kotlin.math.max 13 | 14 | @Composable 15 | fun DataTable( 16 | columns: ColumnBuilder.() -> Unit, 17 | modifier: Modifier = Modifier, 18 | cellPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 5.dp), 19 | divider: @Composable ((rowIndex: Int) -> Unit)? = @Composable { Divider() }, 20 | footer: @Composable (BoxScope.() -> Unit)? = null, 21 | rowsContent: RowsBuilder.() -> Unit 22 | ) { 23 | val columnBuilder = ColumnBuilderImpl().apply { columns() } 24 | val headers = columnBuilder.columns 25 | val rows = RowsBuilderImpl().apply { rowsContent() }.rows.map { it.apply { this.build() } } 26 | 27 | val contentComposable = @Composable { 28 | headers.forEach { (header, modifier, contentAlignment) -> 29 | Box(modifier = modifier.padding(cellPadding), contentAlignment = contentAlignment) { header() } 30 | } 31 | rows.forEach { row -> 32 | row.cells.forEach { (cell, modifier, contentAlignment) -> 33 | Box(modifier = modifier.padding(cellPadding), contentAlignment = contentAlignment) { cell() } 34 | } 35 | } 36 | } 37 | 38 | val backgroundComposables = @Composable { 39 | rows.forEach { Box(modifier = it.modifier.then(Modifier.fillMaxSize())) } 40 | } 41 | 42 | SubcomposeLayout(modifier = modifier) { constraints -> 43 | // Combine subcompose calls for headers and cells 44 | val contentPlaceables = subcompose("content", content = contentComposable) 45 | 46 | // Measure to determine the column widths 47 | val columnWidths = IntArray(headers.size) { 0 } 48 | val rowHeights = IntArray(rows.size + 1) { 0 } 49 | contentPlaceables.chunked(headers.size).forEachIndexed { rowIndex, row -> 50 | row.mapIndexed { columnIndex, placeable -> 51 | placeable.measure(constraints).also { measured -> 52 | columnWidths[columnIndex] = max(columnWidths[columnIndex], measured.width) 53 | rowHeights[rowIndex] = max(rowHeights[rowIndex], measured.height) 54 | } 55 | } 56 | } 57 | 58 | // Calculate the scaling factor if needed 59 | val totalWidth = columnWidths.sum() 60 | 61 | val scale = when { 62 | constraints.maxWidth == Constraints.Infinity -> 1f // add this to avoid infinite width in parent scrolling 63 | totalWidth < constraints.maxWidth -> constraints.maxWidth.toFloat() / totalWidth 64 | totalWidth > constraints.maxWidth -> constraints.maxWidth.toFloat() / totalWidth 65 | else -> 1f 66 | } 67 | 68 | // Apply scaling to column widths 69 | val scaledColumnWidths = columnWidths.map { (it * scale).toInt() } 70 | 71 | // Measure with the scaled column widths 72 | val scaledContentPlaceables = 73 | subcompose("scaledContent", content = contentComposable).mapIndexed { index, measurable -> 74 | val rowHeight = rowHeights[index / headers.size] 75 | val columnWidth = scaledColumnWidths[index % headers.size] 76 | val scaledConstraints = constraints.copy(columnWidth, columnWidth, rowHeight, rowHeight) 77 | measurable.measure(scaledConstraints) 78 | } 79 | 80 | // Split the measured placeables into headers and rows 81 | val scaledHeaderPlaceables = scaledContentPlaceables.take(headers.size) 82 | val scaledCellRowPlaceables = scaledContentPlaceables.drop(headers.size).chunked(headers.size) 83 | 84 | // Calculate the height of each row and the total height 85 | val headerHeight = scaledHeaderPlaceables.maxOf { it.height } 86 | val tableWidth = scaledColumnWidths.sum() 87 | 88 | val headerBackground = subcompose("headerDecoration", columnBuilder.headerBackground).firstOrNull() 89 | ?.measure(constraints.copy(maxWidth = tableWidth, maxHeight = headerHeight)) 90 | 91 | val rowBackgrounds = subcompose("rowBackgrounds", backgroundComposables).mapIndexed { index, measurable -> 92 | measurable.measure(constraints.copy(maxWidth = tableWidth, maxHeight = rowHeights[index + 1])) 93 | } 94 | 95 | val dividerPlacables = subcompose("dividers") { 96 | repeat(rows.size + 1) { divider?.invoke(it) } // dividers = header + rows 97 | }.mapIndexed { rowIndex, mesurable -> 98 | mesurable.measure(constraints.copy(maxWidth = tableWidth, maxHeight = rowHeights[rowIndex])) 99 | } 100 | val dividierHeights = dividerPlacables.map { it.height } 101 | 102 | val footerPlaceable = footer?.let { 103 | val footerComposable = @Composable { Box(modifier = Modifier.padding(cellPadding)) { it() } } 104 | subcompose("footer", footerComposable).firstOrNull() 105 | ?.measure(constraints.copy(minWidth = tableWidth, maxWidth = tableWidth)) 106 | } 107 | val footerHeight = footerPlaceable?.height ?: 0 108 | 109 | val tableHeight = rowHeights.sum() + dividierHeights.sum() + footerHeight 110 | // Layout the headers and cells 111 | layout(tableWidth, tableHeight) { 112 | // header decoration 113 | headerBackground?.place(0, 0) 114 | 115 | // Place headers 116 | var xPosition = 0 117 | var yPosition = 0 118 | scaledHeaderPlaceables.forEach { placeable -> 119 | placeable.place(x = xPosition, y = yPosition) 120 | xPosition += placeable.width 121 | } 122 | 123 | // header divider 124 | yPosition += headerHeight 125 | if (headerBackground == null) { 126 | dividerPlacables[0].place(0, yPosition) 127 | yPosition += dividierHeights[0] 128 | } 129 | 130 | // Place cells 131 | scaledCellRowPlaceables.forEachIndexed { index, row -> 132 | val background = rowBackgrounds[index] 133 | background.place(0, yPosition) 134 | 135 | xPosition = 0 136 | row.forEach { placeable -> 137 | placeable.place(x = xPosition, y = yPosition) 138 | xPosition += placeable.width 139 | } 140 | yPosition += rowHeights[index + 1] 141 | 142 | // Place divider 143 | dividerPlacables[index + 1].place(0, yPosition) 144 | yPosition += dividierHeights[index + 1] 145 | 146 | } 147 | 148 | footerPlaceable?.place(0, yPosition) 149 | } 150 | } 151 | } 152 | 153 | data class TableCell( 154 | val composable: @Composable BoxScope.() -> Unit, 155 | val modifier: Modifier = Modifier, 156 | val contentAlignment: Alignment = Alignment.CenterStart, 157 | ) 158 | 159 | class ColumnBuilderImpl : ColumnBuilder { 160 | val columns = mutableListOf() 161 | var headerBackground: @Composable (() -> Unit) = @Composable {} 162 | 163 | override fun column( 164 | modifier: Modifier, 165 | contentAlignment: Alignment, 166 | composable: @Composable BoxScope.() -> Unit 167 | ) { 168 | columns.add(TableCell(composable, modifier, contentAlignment)) 169 | } 170 | 171 | override fun headerBackground(composable: @Composable () -> Unit) { 172 | headerBackground = @Composable { RowLayout(composable) } 173 | } 174 | } 175 | 176 | @Composable 177 | fun RowLayout(composable: @Composable () -> Unit) { 178 | Layout(composable, modifier = Modifier.fillMaxSize()) { measurables, constraints -> 179 | val placeable = measurables.map { it.measure(constraints) }.first() 180 | layout(constraints.maxWidth, constraints.maxHeight) { 181 | placeable.place(0, 0) 182 | } 183 | } 184 | } 185 | 186 | 187 | class RowBuilderImpl(val build: RowBuilderImpl.() -> Unit, val modifier: Modifier = Modifier) : RowBuilder { 188 | val cells = mutableListOf() 189 | 190 | override fun cell( 191 | modifier: Modifier, 192 | contentAlignment: Alignment, 193 | content: @Composable BoxScope.() -> Unit 194 | ) { 195 | cells.add(TableCell(content, modifier, contentAlignment)) 196 | } 197 | } 198 | 199 | 200 | class RowsBuilderImpl : RowsBuilder { 201 | val rows = mutableListOf() 202 | 203 | override fun row(modifier: Modifier, content: RowBuilderImpl.() -> Unit) { 204 | rows.add(RowBuilderImpl(content, modifier)) 205 | } 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | --------------------------------------------------------------------------------