Here we are again a whole entire month later, and I’ve found a little more time to make some progress on

Cardboard

Where we last left off, I’d written an extremely simple layout model for prototype cards, and I was looking to expand it and then write a renderer. This weekend, I added a notion of Shape elements, which have a fill and stroke, and I defined one type of shape: a Rectangle.

I did handle Rectangles a little strangely, though: there’s both a struct Rectangle and an enum variant Shape::Rectangle. That’s because I expect other layout objects to reference rectangles (perhaps as bounding boxes), but Rust’s type system doesn’t allow for a symbol whose type is one specific enum variant (i.e., I can’t have a struct Text { box: Shape::Rectangle, /* ... */ }).

For the general type for layout elements (Element), I originally assumed that I’d want all the specific elements to have more complex behavior, so I defined it as a trait using a common pattern in Rust to seal a trait by making it depend on another trait in a private subpackage:


pub trait Element: pvt::Sealed {}

pub struct Shape {
    /*  ...  */
}

impl Shape { /*  ...  */ }
impl Element for Shape {}

mod pvt {
    pub trait Sealed {}

    impl Sealed for Shape
}

It’s a little clunky compared to something like Scala (which just has a sealed keyword for excatly this case), but it does the trick. Types defined outside the module can’t implement Element because they must implement pvt::Sealed to do so, but pvt::Sealed is not visible outside the scope of the file it’s defined in.

With Element defined as above, the Rust compiler can’t statically determine a size for it–even though it’s “sealed”, the compiler can’t prove it’s actually sealed, so it can’t bound the size of its implementations. Thus the layout can’t simply contain a Vec<Element>; it needs a Vec<Box<dyn Element>>. That was all well and good until it came time to actually render those elements. I wanted to iterate through the list and pattern-match on the type of the element to determine how to draw it, but Rust doesn’t allow that type of pattern matching because it can’t verify exhaustiveness. I could have moved the element-type-specific rendering behavior to those types (e.g., a Shape could have known how to draw itself), but I didn’t want my layout model to be aware of the rendering layer.

In the end I decided to give up the tiny amount of flexibility gained through avoiding enums, and just redefined Element as an enum with different variants for each type. That in turn simplified the layout type (it gets to simply hold a Vec<Element>) and allowed pattern matching on the variants.


pub enum Element {
    Shape(shape::Shape, style::Stroke, style::Fill),
}

impl Enum {
    /*  ...  */
}

Rendering

Next up I defined a renderer trait (right now it just demands that a renderer can take in some abstract content and output a PNG file) and stubbed out a CairoRenderer. I added cairo-rs, a set of rust bindings to the Cairo gtk graphics library. I had to fiddle with getting dependencies installed, but it was nowhere near as bad as I worried it might be.

Once everything was building, I implemented:

  • card geometry
  • drawing, stroking, and filling rectangles
  • saving the resulting image

and created a binary crate to render a single demo card:

The first card
The very first card

What’s next?

Now that I can draw rectangles, the next step is to define a text element, then hook up some sort of content provider so the text can be varied from card to card. At that point, it’ll be capable of producing very rudimentary playtest cards, and I expect I’ll cut a 1.0 version.

As far as other projects, I have some artwork I purchased earlier this year away at the framer, and I’ve been thinking it might be nice to create an online gallery to display it (in addition to hanging it on my wall).