├── 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 | 
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 |
--------------------------------------------------------------------------------