
Infinite pannable, zoomable canvas for building node-based editors, whiteboards, and diagrams with custom node content, per-node and canvas context menus, Bezier connections, gestures, dynamic nodes, pin-to-front.
A Compose Multiplatform infinite canvas library for building node-based editors, whiteboards, and diagram tools.
pinToFront keeps nodes above others, fixed prevents dragging (both toggleable at runtime)implementation("io.github.xingray:compose-infinite-canvas-core:0.2.0")In gradle/libs.versions.toml:
[versions]
composeInfiniteCanvasCore = "0.2.0"
[libraries]
compose-infinite-canvas-core = { module = "io.github.xingray:compose-infinite-canvas-core", version.ref = "composeInfiniteCanvasCore" }Then in your module's build.gradle.kts:
implementation(libs.compose.infinite.canvas.core)| Platform | Artifact |
|---|---|
| Android | compose-infinite-canvas-core-android |
| JVM (Desktop) | compose-infinite-canvas-core-jvm |
| iOS Arm64 | compose-infinite-canvas-core-iosarm64 |
| iOS Simulator | compose-infinite-canvas-core-iossimulatorarm64 |
| macOS Arm64 | compose-infinite-canvas-core-macosarm64 |
| macOS X64 | compose-infinite-canvas-core-macosx64 |
| JS (Browser) | compose-infinite-canvas-core-js |
| WASM (Browser) | compose-infinite-canvas-core-wasmjs |
Gradle will automatically resolve the correct platform artifact. No need to specify them manually.
import io.github.xingray.compose.infinitecanvas.*
@Composable
fun MyCanvas() {
val canvasState = rememberInfiniteCanvasState()
val node1 = rememberCanvasNodeState(x = 100f, y = 100f)
val node2 = rememberCanvasNodeState(x = 450f, y = 100f)
val nodes = remember(node1, node2) {
listOf(
CanvasNode(
id = "note-1",
modifier = Modifier.width(220.dp),
state = node1,
content = {
Column(Modifier.padding(14.dp)) {
Text("Hello", fontWeight = FontWeight.Bold)
Text("This is a card on the infinite canvas.")
}
},
menu = { onDismiss ->
// your custom right-click menu
},
),
CanvasNode(
id = "note-2",
modifier = Modifier.width(220.dp),
state = node2,
content = {
Column(Modifier.padding(14.dp)) {
Text("World", fontWeight = FontWeight.Bold)
Text("Connect me to the other card!")
}
},
),
)
}
InfiniteCanvas(
modifier = Modifier.fillMaxSize(),
state = canvasState,
nodes = nodes,
menu = { onDismiss ->
// canvas-level right-click menu
},
)
}| API | Description |
|---|---|
InfiniteCanvas(state, nodes, menu) |
Main canvas composable |
CanvasNode(id, modifier, state, content, menu) |
Node with custom content and menu |
CanvasNodeState(x, y, fixed, pinToFront) |
Mutable node position and behavior |
InfiniteCanvasState |
Canvas viewport, selection, connections |
InfiniteCanvasConfig |
Visual config (grid, background, controls) |
rememberInfiniteCanvasState() |
Remember canvas state |
rememberCanvasNodeState(x, y, fixed, pinToFront) |
Remember node state |
compose-infinite-canvas/
├── compose-infinite-canvas-core/ # Library module (publishable)
│ └── src/commonMain/
│ └── io.github.xingray.compose.infinitecanvas/
│ ├── CanvasNode.kt # Node definition
│ ├── CanvasNodeState.kt # Node mutable state
│ ├── InfiniteCanvas.kt # Main composable
│ ├── InfiniteCanvasState.kt # Canvas state management
│ ├── InfiniteCanvasConfig.kt # Canvas visual config
│ ├── Connection.kt # Connection data model
│ ├── ViewportState.kt # Pan & zoom state
│ ├── MenuPositionUtils.kt # Menu positioning
│ ├── connection/
│ │ └── ConnectionRenderer.kt # Bezier curve rendering
│ ├── element/
│ │ └── CardElementView.kt # Node chrome (border, anchors)
│ └── gesture/
│ └── CanvasGestureHandler.kt # Gesture handling
├── sample/ # Demo app (KMP: JVM, JS, WASM, Android, iOS)
└── sample-android/ # Android demo app entry point
# Build the library
./gradlew :compose-infinite-canvas-core:build
# Run the demo app (Desktop)
./gradlew :sample:runThe 0.2.0 release is a breaking API change:
| 0.1.x | 0.2.0 |
|---|---|
CanvasViewModel |
InfiniteCanvasState / rememberInfiniteCanvasState()
|
CardElement(title, content) |
CanvasNode(content = { ... }) |
CanvasElement sealed class |
Removed |
InfiniteCanvas(viewModel) |
InfiniteCanvas(state, nodes, menu) |
| Fixed context menus | Custom per-node and canvas menus |
Artifact: compose-infinite-canvas
|
Artifact: compose-infinite-canvas-core
|
This project is open source. See the repository for license details.
一个基于 Compose Multiplatform 的无限画布库,可用于构建节点编辑器、白板和图表工具。
pinToFront 置顶显示,fixed 禁止拖拽(均可运行时切换)implementation("io.github.xingray:compose-infinite-canvas-core:0.2.0")在 gradle/libs.versions.toml 中添加:
[versions]
composeInfiniteCanvasCore = "0.2.0"
[libraries]
compose-infinite-canvas-core = { module = "io.github.xingray:compose-infinite-canvas-core", version.ref = "composeInfiniteCanvasCore" }然后在模块的 build.gradle.kts 中引用:
implementation(libs.compose.infinite.canvas.core)| 平台 | Artifact |
|---|---|
| Android | compose-infinite-canvas-core-android |
| JVM (桌面端) | compose-infinite-canvas-core-jvm |
| iOS Arm64 | compose-infinite-canvas-core-iosarm64 |
| iOS Simulator | compose-infinite-canvas-core-iossimulatorarm64 |
| macOS Arm64 | compose-infinite-canvas-core-macosarm64 |
| macOS X64 | compose-infinite-canvas-core-macosx64 |
| JS (浏览器) | compose-infinite-canvas-core-js |
| WASM (浏览器) | compose-infinite-canvas-core-wasmjs |
Gradle 会自动解析对应平台的 artifact,无需手动指定。
import io.github.xingray.compose.infinitecanvas.*
@Composable
fun MyCanvas() {
val canvasState = rememberInfiniteCanvasState()
val node1 = rememberCanvasNodeState(x = 100f, y = 100f)
val node2 = rememberCanvasNodeState(x = 450f, y = 100f)
val nodes = remember(node1, node2) {
listOf(
CanvasNode(
id = "note-1",
modifier = Modifier.width(220.dp),
state = node1,
content = {
Column(Modifier.padding(14.dp)) {
Text("你好", fontWeight = FontWeight.Bold)
Text("这是无限画布上的一张卡片。")
}
},
menu = { onDismiss ->
// 自定义右键菜单
},
),
CanvasNode(
id = "note-2",
modifier = Modifier.width(220.dp),
state = node2,
content = {
Column(Modifier.padding(14.dp)) {
Text("世界", fontWeight = FontWeight.Bold)
Text("把我和另一张卡片连接起来!")
}
},
),
)
}
InfiniteCanvas(
modifier = Modifier.fillMaxSize(),
state = canvasState,
nodes = nodes,
menu = { onDismiss ->
// 画布级右键菜单
},
)
}| API | 说明 |
|---|---|
InfiniteCanvas(state, nodes, menu) |
主画布组件 |
CanvasNode(id, modifier, state, content, menu) |
自定义内容和菜单的节点 |
CanvasNodeState(x, y, fixed, pinToFront) |
节点位置和行为的可变状态 |
InfiniteCanvasState |
画布视口、选择、连接线状态 |
InfiniteCanvasConfig |
外观配置(网格、背景、控件) |
rememberInfiniteCanvasState() |
记住画布状态 |
rememberCanvasNodeState(x, y, fixed, pinToFront) |
记住节点状态 |
# 构建库
./gradlew :compose-infinite-canvas-core:build
# 运行示例应用(桌面端)
./gradlew :sample:run0.2.0 是破坏性 API 变更:
| 0.1.x | 0.2.0 |
|---|---|
CanvasViewModel |
InfiniteCanvasState / rememberInfiniteCanvasState()
|
CardElement(title, content) |
CanvasNode(content = { ... }) |
CanvasElement sealed class |
已移除 |
InfiniteCanvas(viewModel) |
InfiniteCanvas(state, nodes, menu) |
| 固定的右键菜单 | 自定义节点级和画布级菜单 |
Artifact: compose-infinite-canvas
|
Artifact: compose-infinite-canvas-core
|
A Compose Multiplatform infinite canvas library for building node-based editors, whiteboards, and diagram tools.
pinToFront keeps nodes above others, fixed prevents dragging (both toggleable at runtime)implementation("io.github.xingray:compose-infinite-canvas-core:0.2.0")In gradle/libs.versions.toml:
[versions]
composeInfiniteCanvasCore = "0.2.0"
[libraries]
compose-infinite-canvas-core = { module = "io.github.xingray:compose-infinite-canvas-core", version.ref = "composeInfiniteCanvasCore" }Then in your module's build.gradle.kts:
implementation(libs.compose.infinite.canvas.core)| Platform | Artifact |
|---|---|
| Android | compose-infinite-canvas-core-android |
| JVM (Desktop) | compose-infinite-canvas-core-jvm |
| iOS Arm64 | compose-infinite-canvas-core-iosarm64 |
| iOS Simulator | compose-infinite-canvas-core-iossimulatorarm64 |
| macOS Arm64 | compose-infinite-canvas-core-macosarm64 |
| macOS X64 | compose-infinite-canvas-core-macosx64 |
| JS (Browser) | compose-infinite-canvas-core-js |
| WASM (Browser) | compose-infinite-canvas-core-wasmjs |
Gradle will automatically resolve the correct platform artifact. No need to specify them manually.
import io.github.xingray.compose.infinitecanvas.*
@Composable
fun MyCanvas() {
val canvasState = rememberInfiniteCanvasState()
val node1 = rememberCanvasNodeState(x = 100f, y = 100f)
val node2 = rememberCanvasNodeState(x = 450f, y = 100f)
val nodes = remember(node1, node2) {
listOf(
CanvasNode(
id = "note-1",
modifier = Modifier.width(220.dp),
state = node1,
content = {
Column(Modifier.padding(14.dp)) {
Text("Hello", fontWeight = FontWeight.Bold)
Text("This is a card on the infinite canvas.")
}
},
menu = { onDismiss ->
// your custom right-click menu
},
),
CanvasNode(
id = "note-2",
modifier = Modifier.width(220.dp),
state = node2,
content = {
Column(Modifier.padding(14.dp)) {
Text("World", fontWeight = FontWeight.Bold)
Text("Connect me to the other card!")
}
},
),
)
}
InfiniteCanvas(
modifier = Modifier.fillMaxSize(),
state = canvasState,
nodes = nodes,
menu = { onDismiss ->
// canvas-level right-click menu
},
)
}| API | Description |
|---|---|
InfiniteCanvas(state, nodes, menu) |
Main canvas composable |
CanvasNode(id, modifier, state, content, menu) |
Node with custom content and menu |
CanvasNodeState(x, y, fixed, pinToFront) |
Mutable node position and behavior |
InfiniteCanvasState |
Canvas viewport, selection, connections |
InfiniteCanvasConfig |
Visual config (grid, background, controls) |
rememberInfiniteCanvasState() |
Remember canvas state |
rememberCanvasNodeState(x, y, fixed, pinToFront) |
Remember node state |
compose-infinite-canvas/
├── compose-infinite-canvas-core/ # Library module (publishable)
│ └── src/commonMain/
│ └── io.github.xingray.compose.infinitecanvas/
│ ├── CanvasNode.kt # Node definition
│ ├── CanvasNodeState.kt # Node mutable state
│ ├── InfiniteCanvas.kt # Main composable
│ ├── InfiniteCanvasState.kt # Canvas state management
│ ├── InfiniteCanvasConfig.kt # Canvas visual config
│ ├── Connection.kt # Connection data model
│ ├── ViewportState.kt # Pan & zoom state
│ ├── MenuPositionUtils.kt # Menu positioning
│ ├── connection/
│ │ └── ConnectionRenderer.kt # Bezier curve rendering
│ ├── element/
│ │ └── CardElementView.kt # Node chrome (border, anchors)
│ └── gesture/
│ └── CanvasGestureHandler.kt # Gesture handling
├── sample/ # Demo app (KMP: JVM, JS, WASM, Android, iOS)
└── sample-android/ # Android demo app entry point
# Build the library
./gradlew :compose-infinite-canvas-core:build
# Run the demo app (Desktop)
./gradlew :sample:runThe 0.2.0 release is a breaking API change:
| 0.1.x | 0.2.0 |
|---|---|
CanvasViewModel |
InfiniteCanvasState / rememberInfiniteCanvasState()
|
CardElement(title, content) |
CanvasNode(content = { ... }) |
CanvasElement sealed class |
Removed |
InfiniteCanvas(viewModel) |
InfiniteCanvas(state, nodes, menu) |
| Fixed context menus | Custom per-node and canvas menus |
Artifact: compose-infinite-canvas
|
Artifact: compose-infinite-canvas-core
|
This project is open source. See the repository for license details.
一个基于 Compose Multiplatform 的无限画布库,可用于构建节点编辑器、白板和图表工具。
pinToFront 置顶显示,fixed 禁止拖拽(均可运行时切换)implementation("io.github.xingray:compose-infinite-canvas-core:0.2.0")在 gradle/libs.versions.toml 中添加:
[versions]
composeInfiniteCanvasCore = "0.2.0"
[libraries]
compose-infinite-canvas-core = { module = "io.github.xingray:compose-infinite-canvas-core", version.ref = "composeInfiniteCanvasCore" }然后在模块的 build.gradle.kts 中引用:
implementation(libs.compose.infinite.canvas.core)| 平台 | Artifact |
|---|---|
| Android | compose-infinite-canvas-core-android |
| JVM (桌面端) | compose-infinite-canvas-core-jvm |
| iOS Arm64 | compose-infinite-canvas-core-iosarm64 |
| iOS Simulator | compose-infinite-canvas-core-iossimulatorarm64 |
| macOS Arm64 | compose-infinite-canvas-core-macosarm64 |
| macOS X64 | compose-infinite-canvas-core-macosx64 |
| JS (浏览器) | compose-infinite-canvas-core-js |
| WASM (浏览器) | compose-infinite-canvas-core-wasmjs |
Gradle 会自动解析对应平台的 artifact,无需手动指定。
import io.github.xingray.compose.infinitecanvas.*
@Composable
fun MyCanvas() {
val canvasState = rememberInfiniteCanvasState()
val node1 = rememberCanvasNodeState(x = 100f, y = 100f)
val node2 = rememberCanvasNodeState(x = 450f, y = 100f)
val nodes = remember(node1, node2) {
listOf(
CanvasNode(
id = "note-1",
modifier = Modifier.width(220.dp),
state = node1,
content = {
Column(Modifier.padding(14.dp)) {
Text("你好", fontWeight = FontWeight.Bold)
Text("这是无限画布上的一张卡片。")
}
},
menu = { onDismiss ->
// 自定义右键菜单
},
),
CanvasNode(
id = "note-2",
modifier = Modifier.width(220.dp),
state = node2,
content = {
Column(Modifier.padding(14.dp)) {
Text("世界", fontWeight = FontWeight.Bold)
Text("把我和另一张卡片连接起来!")
}
},
),
)
}
InfiniteCanvas(
modifier = Modifier.fillMaxSize(),
state = canvasState,
nodes = nodes,
menu = { onDismiss ->
// 画布级右键菜单
},
)
}| API | 说明 |
|---|---|
InfiniteCanvas(state, nodes, menu) |
主画布组件 |
CanvasNode(id, modifier, state, content, menu) |
自定义内容和菜单的节点 |
CanvasNodeState(x, y, fixed, pinToFront) |
节点位置和行为的可变状态 |
InfiniteCanvasState |
画布视口、选择、连接线状态 |
InfiniteCanvasConfig |
外观配置(网格、背景、控件) |
rememberInfiniteCanvasState() |
记住画布状态 |
rememberCanvasNodeState(x, y, fixed, pinToFront) |
记住节点状态 |
# 构建库
./gradlew :compose-infinite-canvas-core:build
# 运行示例应用(桌面端)
./gradlew :sample:run0.2.0 是破坏性 API 变更:
| 0.1.x | 0.2.0 |
|---|---|
CanvasViewModel |
InfiniteCanvasState / rememberInfiniteCanvasState()
|
CardElement(title, content) |
CanvasNode(content = { ... }) |
CanvasElement sealed class |
已移除 |
InfiniteCanvas(viewModel) |
InfiniteCanvas(state, nodes, menu) |
| 固定的右键菜单 | 自定义节点级和画布级菜单 |
Artifact: compose-infinite-canvas
|
Artifact: compose-infinite-canvas-core
|