
Cassowary constraint-solving implementation for UI layout, low-level solver API with weighted strengths, editable variables for interactive updates, and dual error styles (exceptions or Result-based).
A Kotlin Multiplatform Native implementation of the Cassowary constraint solving algorithm (Badros et. al 2001). This is a port of the Rust kasuari library by the Ratatui team.
Kasuari is the Indonesian name for the Cassowary bird.
Cassowary is designed for solving constraints to lay out user interfaces. Constraints typically take the form "this button must line up with this text box", or "this box should try to be 3 times the size of this other box". Its most popular incarnation by far is in Apple's AutoLayout system for macOS and iOS user interfaces.
This library is a low-level interface to the solving algorithm. It does not have any intrinsic knowledge of common user interface conventions like rectangular regions or even two dimensions. These abstractions belong in a higher-level library.
This library is not yet published to Maven Central. The recommended approach is to include it as a git submodule or vendored dependency:
git submodule add https://github.com/KotlinMania/kasuari-kotlin.gitThen in your settings.gradle.kts:
include(":kasuari-kotlin")And in your module's build.gradle.kts:
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":kasuari-kotlin"))
}
}
}
}Once published to Maven Central, you'll be able to add it directly:
dependencies {
implementation("io.github.kotlinmania:kasuari-kotlin:1.0.0")
}import kasuari.*
// Create a solver
val solver = Solver.new()
// Create variables
val left = Variable.new()
val width = Variable.new()
val right = Variable.new()
// Add constraints: right == left + width
solver.addConstraint(
right with WeightedRelation.EQ(Strength.REQUIRED) to (left + width)
)
// left == 0
solver.addConstraint(
left with WeightedRelation.EQ(Strength.REQUIRED) to 0.0
)
// width == 100 (strong, not required)
solver.addConstraint(
width with WeightedRelation.EQ(Strength.STRONG) to 100.0
)
// Read the solution
println("left: ${solver.getValue(left)}") // 0.0
println("width: ${solver.getValue(width)}") // 100.0
println("right: ${solver.getValue(right)}") // 100.0For interactive applications, use edit variables to dynamically change values:
val solver = Solver.new()
val x = Variable.new()
// Add a constraint that x >= 0
solver.addConstraint(x with WeightedRelation.GE(Strength.REQUIRED) to 0.0)
// Register x as an edit variable
solver.addEditVariable(x, Strength.STRONG)
// Suggest values for x
solver.suggestValue(x, 50.0)
println(solver.getValue(x)) // 50.0
solver.suggestValue(x, -10.0)
println(solver.getValue(x)) // 0.0 (constrained to >= 0)The library provides two styles of error handling:
try {
solver.addConstraint(constraint)
} catch (e: AddConstraintError.DuplicateConstraint) {
println("Constraint already exists")
} catch (e: AddConstraintError.UnsatisfiableConstraint) {
println("Constraint conflicts with existing constraints")
}when (val result = solver.tryAddConstraint(constraint)) {
is Result.Ok -> println("Constraint added")
is Result.Err -> when (result.error) {
is AddConstraintError.DuplicateConstraint -> println("Already exists")
is AddConstraintError.UnsatisfiableConstraint -> println("Conflicts")
is AddConstraintError.InternalSolver -> println("Internal error")
}
}Constraints have strengths that determine priority when conflicts arise:
Strength.REQUIRED - Must be satisfied (solver fails if impossible)Strength.STRONG - High priority, but can be violatedStrength.MEDIUM - Medium priorityStrength.WEAK - Low priority, used for defaults/preferences// Required: x must equal 100
val required = x with WeightedRelation.EQ(Strength.REQUIRED) to 100.0
// Strong: x should be at least 0
val strong = x with WeightedRelation.GE(Strength.STRONG) to 0.0
// Weak: x prefers to be 50
val weak = x with WeightedRelation.EQ(Strength.WEAK) to 50.0Licensed under
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the MIT license, shall be licensed as above, without any additional terms or conditions.
This Kotlin Multiplatform port was created by Sydney Renee of The Solace Project for KotlinMania.
Special thanks to the original authors and contributors:
A Kotlin Multiplatform Native implementation of the Cassowary constraint solving algorithm (Badros et. al 2001). This is a port of the Rust kasuari library by the Ratatui team.
Kasuari is the Indonesian name for the Cassowary bird.
Cassowary is designed for solving constraints to lay out user interfaces. Constraints typically take the form "this button must line up with this text box", or "this box should try to be 3 times the size of this other box". Its most popular incarnation by far is in Apple's AutoLayout system for macOS and iOS user interfaces.
This library is a low-level interface to the solving algorithm. It does not have any intrinsic knowledge of common user interface conventions like rectangular regions or even two dimensions. These abstractions belong in a higher-level library.
This library is not yet published to Maven Central. The recommended approach is to include it as a git submodule or vendored dependency:
git submodule add https://github.com/KotlinMania/kasuari-kotlin.gitThen in your settings.gradle.kts:
include(":kasuari-kotlin")And in your module's build.gradle.kts:
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":kasuari-kotlin"))
}
}
}
}Once published to Maven Central, you'll be able to add it directly:
dependencies {
implementation("io.github.kotlinmania:kasuari-kotlin:1.0.0")
}import kasuari.*
// Create a solver
val solver = Solver.new()
// Create variables
val left = Variable.new()
val width = Variable.new()
val right = Variable.new()
// Add constraints: right == left + width
solver.addConstraint(
right with WeightedRelation.EQ(Strength.REQUIRED) to (left + width)
)
// left == 0
solver.addConstraint(
left with WeightedRelation.EQ(Strength.REQUIRED) to 0.0
)
// width == 100 (strong, not required)
solver.addConstraint(
width with WeightedRelation.EQ(Strength.STRONG) to 100.0
)
// Read the solution
println("left: ${solver.getValue(left)}") // 0.0
println("width: ${solver.getValue(width)}") // 100.0
println("right: ${solver.getValue(right)}") // 100.0For interactive applications, use edit variables to dynamically change values:
val solver = Solver.new()
val x = Variable.new()
// Add a constraint that x >= 0
solver.addConstraint(x with WeightedRelation.GE(Strength.REQUIRED) to 0.0)
// Register x as an edit variable
solver.addEditVariable(x, Strength.STRONG)
// Suggest values for x
solver.suggestValue(x, 50.0)
println(solver.getValue(x)) // 50.0
solver.suggestValue(x, -10.0)
println(solver.getValue(x)) // 0.0 (constrained to >= 0)The library provides two styles of error handling:
try {
solver.addConstraint(constraint)
} catch (e: AddConstraintError.DuplicateConstraint) {
println("Constraint already exists")
} catch (e: AddConstraintError.UnsatisfiableConstraint) {
println("Constraint conflicts with existing constraints")
}when (val result = solver.tryAddConstraint(constraint)) {
is Result.Ok -> println("Constraint added")
is Result.Err -> when (result.error) {
is AddConstraintError.DuplicateConstraint -> println("Already exists")
is AddConstraintError.UnsatisfiableConstraint -> println("Conflicts")
is AddConstraintError.InternalSolver -> println("Internal error")
}
}Constraints have strengths that determine priority when conflicts arise:
Strength.REQUIRED - Must be satisfied (solver fails if impossible)Strength.STRONG - High priority, but can be violatedStrength.MEDIUM - Medium priorityStrength.WEAK - Low priority, used for defaults/preferences// Required: x must equal 100
val required = x with WeightedRelation.EQ(Strength.REQUIRED) to 100.0
// Strong: x should be at least 0
val strong = x with WeightedRelation.GE(Strength.STRONG) to 0.0
// Weak: x prefers to be 50
val weak = x with WeightedRelation.EQ(Strength.WEAK) to 50.0Licensed under
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the MIT license, shall be licensed as above, without any additional terms or conditions.
This Kotlin Multiplatform port was created by Sydney Renee of The Solace Project for KotlinMania.
Special thanks to the original authors and contributors: