Expand description

This crate contains an implementation of the Cassowary constraint solving algorithm, based upon the work by G.J. Badros et al. in 2001. This algorithm is designed primarily for use constraining elements in user interfaces, but works well for many constraints that use floats. Constraints are linear combinations of the problem variables. The notable features of Cassowary that make it ideal for user interfaces are that it is incremental (i.e. you can add and remove constraints at runtime and it will perform the minimum work to update the result) and that the constraints can be violated if necessary, with the order in which they are violated specified by setting a “strength” for each constraint. This allows the solution to gracefully degrade, which is useful for when a user interface needs to compromise on its constraints in order to still be able to display something.

Constraint builder

This crate aims to provide a builder for describing linear constraints as naturally as possible.

For example, for the constraint (a + b) * 2 + c >= d + 1 with strength s, the code to use is

((a + b) * 2.0 + c)
    .is_ge(d + 1.0)
    .with_strength(s)

This crate also provides the derive_syntax_for macro, which allows you to use your own named variables.

A simple example

Imagine a layout consisting of two elements laid out horizontally. For small window widths the elements should compress to fit, but if there is enough space they should display at their preferred widths. The first element will align to the left, and the second to the right. For this example we will ignore vertical layout.

use casuarius::*;

// We define the variables required using an Element type with left
// and right edges, and the width of the window.

struct Element {
    left: Variable,
    right: Variable
}
let box1 = Element {
    left: Variable("box1.left"),
    right: Variable("box1.right")
};

let window_width = Variable("window_width");

let box2 = Element {
    left: Variable("box2.left"),
    right: Variable("box2.right")
};

// Now we set up the solver and constraints.

let mut solver = Solver::<Variable>::default();
solver.add_constraints(vec![
    window_width.is_ge(0.0), // positive window width
    box1.left.is(0.0), // left align
    box2.right.is(window_width), // right align
    box2.left.is_ge(box1.right), // no overlap
    // positive widths
    box1.left.is_le(box1.right),
    box2.left.is_le(box2.right),
    // preferred widths:
    (box1.right - box1.left).is(50.0).with_strength(WEAK),
    (box2.right - box2.left).is(100.0).with_strength(WEAK)
]).unwrap();

// The window width is currently free to take any positive value. Let's constrain it to a particular value.
// Since for this example we will repeatedly change the window width, it is most efficient to use an
// "edit variable", instead of repeatedly removing and adding constraints (note that for efficiency
// reasons we cannot edit a normal constraint that has been added to the solver).

solver.add_edit_variable(window_width, STRONG).unwrap();
solver.suggest_value(window_width, 300.0).unwrap();

// This value of 300 is enough to fit both boxes in with room to spare, so let's check that this is the case.
// We can fetch a list of changes to the values of variables in the solver. Using the pretty printer defined
// earlier we can see what values our variables now hold.

let mut print_changes = || {
    println!("Changes:");
    solver
        .fetch_changes()
        .iter()
        .map(|(var, val)| println!("{}: {}", var.0, val));
};
print_changes();

// Changes:
// window_width: 300
// box1.left -0
// box1.right 50
// box2.left 200
// box2.right 300

// Note that the value of `box1.left` is not mentioned. This is because `solver.fetch_changes` only lists
// *changes* to variables, and since each variable starts in the solver with a value of zero, any values that
// have not changed from zero will not be reported.

// Just to be thorough, let's assert our current values:
let ww = solver.get_value(window_width);
let b1l = solver.get_value(box1.left);
let b1r = solver.get_value(box1.right);
let b2l = solver.get_value(box2.left);
let b2r = solver.get_value(box2.right);
println!("window_width: {}", ww);
println!("box1.left {}", b1l);
println!("box1.right {}", b1r);
println!("box2.left {}", b2l);
println!("box2.right {}", b2r);
assert!(ww >= 0.0);
assert_eq!(0.0, b1l);
assert_eq!(ww, b2r, "box2.right ({}) != ww ({})", b2r, ww);
assert!(b2l >= b1r);
assert!(b1l <= b1r);
assert!(b2l <= b2r);
assert_eq!(50.0, b1r - b1l, "box1 width");
assert_eq!(100.0, b2r - b2l, "box2 width");

// Now let's try compressing the window so that the boxes can't take up their preferred widths.

solver.suggest_value(window_width, 75.0);

// Now the solver can't satisfy all of the constraints. It will pick at least one of the weakest
// constraints to violate. In this case it will be one or both of the preferred widths.

let expected_changes = vec![
    (box2.right, 75.0),
    (box2.left, 0.0),
    (box1.right, 0.0),
    (window_width, 75.0),
];
let changes = solver.fetch_changes().iter().copied().collect::<Vec<_>>();

assert_eq!(expected_changes, changes);

// In a user interface this is not likely a result we would prefer. The solution is to add another constraint
// to control the behaviour when the preferred widths cannot both be satisfied. In this example we are going
// to constrain the boxes to try to maintain a ratio between their widths.

solver.add_constraint(
    ((box1.right - box1.left) / 50.0f64)
        .is((box2.right - box2.left) / 100.0f64)
).unwrap();

// Now the result gives values that maintain the ratio between the sizes of the two boxes:

let box1_width = solver.get_value(box1.right) - solver.get_value(box1.left);
let box2_width = solver.get_value(box2.right) - solver.get_value(box2.left);
assert_eq!(box1_width / 50.0, box2_width / 100.0, "box width ratios");

This example may have appeared somewhat contrived, but hopefully it shows the power of the cassowary algorithm for laying out user interfaces.

One thing that this example exposes is that this crate is a rather low level library. It does not have any inherent knowledge of user interfaces, directions or boxes. Thus for use in a user interface this crate should ideally be wrapped by a higher level API, which is outside the scope of this crate.

Re-exports

pub use strength::MEDIUM;
pub use strength::REQUIRED;
pub use strength::STRONG;
pub use strength::WEAK;

Modules

Contains useful constants and functions for producing strengths for use in the constraint solver. Each constraint added to the solver has an associated strength specifying the precedence the solver should impose when choosing which constraints to enforce. It will try to enforce all constraints, but if that is impossible the lowest strength constraints are the first to be violated.

Macros

Derives operator support for your cassowary solver variable type. This allows you to use your variable type in writing expressions, to a limited extent.

Structs

A constraint, consisting of an equation governed by an expression and a relational operator, and an associated strength.

The possible error conditions that Solver::commit_edit can fail with.

An expression that can be the left hand or right hand side of a constraint equation. It is a linear combination of variables, i.e. a sum of variables weighted by coefficients, plus an optional constant.

This is an intermediate type used in the syntactic sugar for specifying constraints. You should not use it directly.

A constraint solver using the Cassowary algorithm. For proper usage please see the top level crate documentation.

A variable and a coefficient to multiply that variable by. This is a sub-expression in a constraint equation.

A generic variable that can be created with a &'static str.

Enums

The possible error conditions that Solver::add_constraint can fail with.

The possible error conditions that Solver::add_edit_variable can fail with.

The possible relations that a constraint can specify.

The possible error conditions that Solver::remove_constraint can fail with.

The possible error conditions that Solver::remove_edit_variable can fail with.

The possible error conditions that Solver::suggest_value can fail with.

This is part of the syntactic sugar used for specifying constraints. This enum should be used as part of a constraint expression. See the module documentation for more information.

Traits

A trait for creating constraints using custom variable types.