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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,70 @@ jobs:

- name: Build release product
run: swift build -c release

kotlin:
name: Kotlin tests and publication build
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5

- name: Run Kotlin tests
run: ./gradlew :AstrolabeProtocolKotlin:test

- name: Build Kotlin publication
run: ./gradlew :AstrolabeProtocolKotlin:publishToMavenLocal

publish-kotlin:
name: Publish Kotlin artifact
if: startsWith(github.ref, 'refs/tags/')
needs:
- contract
- swift
- kotlin
runs-on: ubuntu-24.04
timeout-minutes: 20

steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: "22"

- name: Verify release tag
run: >-
node -e 'const version = require("./package.json").version;
if (process.env.GITHUB_REF_NAME !== version) {
throw new Error(`Tag ${process.env.GITHUB_REF_NAME} does not match version ${version}`);
}'

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5

- name: Publish and release Kotlin artifact
run: ./gradlew :AstrolabeProtocolKotlin:publishAndReleaseToMavenCentral
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
.build/
.gradle/
.codegraph/
.omo/
.swiftpm/
DerivedData/
node_modules/
**/build/
*.xcuserstate
.DS_Store
97 changes: 97 additions & 0 deletions AstrolabeProtocolKotlin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.maven.publish)
}

group = "io.github.regulusleow"
version = providers.gradleProperty("astrolabeVersion").get()

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

kotlin {
jvmToolchain(17)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}

dependencies {
api(libs.kotlinx.serialization.json)
testImplementation(kotlin("test"))
}

sourceSets {
test {
resources.srcDir(rootProject.layout.projectDirectory.dir("Fixtures"))
}
}

tasks.test {
useJUnitPlatform()
}

mavenPublishing {
publishToMavenCentral()
if (!providers.gradleProperty("signingInMemoryKey").orNull.isNullOrBlank()) {
signAllPublications()
}
coordinates(
groupId = project.group.toString(),
artifactId = "astrolabe-protocol-kotlin",
version = project.version.toString()
)

pom {
name.set("Astrolabe Protocol Kotlin")
description.set("Platform-neutral Kotlin implementation of the Astrolabe Wire Protocol.")
inceptionYear.set("2026")
url.set("https://github.com/regulusleow/astrolabe-protocol")

licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
}
}

developers {
developer {
id.set("regulusleow")
name.set("Regulus Leow")
url.set("https://github.com/regulusleow")
}
}

scm {
url.set("https://github.com/regulusleow/astrolabe-protocol")
connection.set("scm:git:https://github.com/regulusleow/astrolabe-protocol.git")
developerConnection.set("scm:git:ssh://git@github.com/regulusleow/astrolabe-protocol.git")
}
}
}

val requiredCentralPublishingProperties = listOf(
"mavenCentralUsername",
"mavenCentralPassword",
"signingInMemoryKey"
)

tasks.matching { it.name.contains("MavenCentral") }.configureEach {
doFirst {
val missingProperties = requiredCentralPublishingProperties.filterNot {
!providers.gradleProperty(it).orNull.isNullOrBlank()
}
if (missingProperties.isNotEmpty()) {
throw GradleException(
"Missing Maven Central publishing properties: ${missingProperties.joinToString()}"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// RuntimeColor.kt
// astrolabe-protocol
//
// Created by 轩辕十四 on 2026/7/20.
//

package dev.astrolabe.protocol

import kotlinx.serialization.Serializable

/** Color components expressed in a declared color space. */
@Serializable
public data class RuntimeColor(
/** Color-space identifier used by the components. */
public val colorSpace: String,
/** Red component in the inclusive range from zero to one. */
public val red: Double,
/** Green component in the inclusive range from zero to one. */
public val green: Double,
/** Blue component in the inclusive range from zero to one. */
public val blue: Double,
/** Alpha component in the inclusive range from zero to one. */
public val alpha: Double
) {
init {
require(colorSpace.isNotEmpty()) { "Color space cannot be empty" }
require(listOf(red, green, blue, alpha).all { it in 0.0..1.0 }) {
"Color components must be between zero and one"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//
// RuntimeGeometry.kt
// astrolabe-protocol
//
// Created by 轩辕十四 on 2026/7/20.
//

package dev.astrolabe.protocol

import kotlinx.serialization.Serializable

/** Unit used by runtime measurements. */
@Serializable
public enum class RuntimeMeasurementUnit {
logical,
scaledLogical,
pixel
}

/** Coordinate space containing a runtime geometry value. */
@Serializable
public enum class RuntimeCoordinateSpace {
local,
parent,
screen,
viewport
}

/** Scalar measurement with an explicit unit. */
@Serializable
public data class RuntimeMeasurement(
/** Numeric magnitude. */
public val value: Double,
/** Unit used by the magnitude. */
public val unit: RuntimeMeasurementUnit
)

/** Point in an explicitly declared coordinate space. */
@Serializable
public data class RuntimeCoordinatePoint(
/** Horizontal coordinate. */
public val x: Double,
/** Vertical coordinate. */
public val y: Double,
/** Coordinate space containing the point. */
public val coordinateSpace: RuntimeCoordinateSpace,
/** Unit used by both coordinates. */
public val unit: RuntimeMeasurementUnit
) {
init {
require(unit != RuntimeMeasurementUnit.scaledLogical) {
"Coordinate point unit must be logical or pixel"
}
}
}

/** Two-dimensional measured extent. */
@Serializable
public data class RuntimeMeasuredSize(
/** Non-negative horizontal extent. */
public val width: Double,
/** Non-negative vertical extent. */
public val height: Double,
/** Unit used by both extents. */
public val unit: RuntimeMeasurementUnit
) {
init {
require(width >= 0.0 && height >= 0.0) { "Measured size dimensions cannot be negative" }
require(unit != RuntimeMeasurementUnit.scaledLogical) {
"Measured size unit must be logical or pixel"
}
}
}

/** Two-dimensional signed delta. */
@Serializable
public data class RuntimeVector(
/** Horizontal delta. */
public val dx: Double,
/** Vertical delta. */
public val dy: Double,
/** Unit used by both deltas. */
public val unit: RuntimeMeasurementUnit
) {
init {
require(unit != RuntimeMeasurementUnit.scaledLogical) {
"Vector unit must be logical or pixel"
}
}
}

/** Insets measured from four edges. */
@Serializable
public data class RuntimeInsets(
/** Top inset. */
public val top: Double,
/** Left inset. */
public val left: Double,
/** Bottom inset. */
public val bottom: Double,
/** Right inset. */
public val right: Double,
/** Unit used by every edge. */
public val unit: RuntimeMeasurementUnit
) {
init {
require(unit != RuntimeMeasurementUnit.scaledLogical) {
"Insets unit must be logical or pixel"
}
}
}

/** Rectangle in an explicitly declared coordinate space. */
@Serializable
public data class RuntimeCoordinateRect(
/** Horizontal origin. */
public val x: Double,
/** Vertical origin. */
public val y: Double,
/** Non-negative horizontal extent. */
public val width: Double,
/** Non-negative vertical extent. */
public val height: Double,
/** Coordinate space containing the rectangle. */
public val coordinateSpace: RuntimeCoordinateSpace,
/** Unit used by all rectangle components. */
public val unit: RuntimeMeasurementUnit
) {
init {
require(width >= 0.0 && height >= 0.0) { "Rectangle dimensions cannot be negative" }
require(unit != RuntimeMeasurementUnit.scaledLogical) {
"Rectangle unit must be logical or pixel"
}
}
}

/** Per-axis logical-to-pixel conversion. */
@Serializable
public data class RuntimeScale(
/** Horizontal scale. */
public val x: Double,
/** Vertical scale. */
public val y: Double
) {
init {
require(x > 0.0 && y > 0.0) { "Scale components must be greater than zero" }
}
}

/** Display facts required for coordinate conversion. */
@Serializable
public data class RuntimeDisplayInfo(
/** Display dimensions in logical layout units. */
public val logicalSize: RuntimeMeasuredSize,
/** Display dimensions in physical pixels. */
public val pixelSize: RuntimeMeasuredSize,
/** Per-axis conversion from logical units to pixels. */
public val logicalToPixelScale: RuntimeScale,
/** Maximum refresh rate when reported by the platform. */
public val maximumRefreshRate: Double?
) {
init {
require(logicalSize.unit == RuntimeMeasurementUnit.logical) {
"Logical display size must use logical units"
}
require(pixelSize.unit == RuntimeMeasurementUnit.pixel) {
"Pixel display size must use pixel units"
}
require(maximumRefreshRate == null || maximumRefreshRate > 0.0) {
"Maximum refresh rate must be greater than zero"
}
}
}
Loading