I've been dreaming up a way to replace seamstress's dependence on SDL for about a month now. My goals (which I'll describe below) are such that I'll be writing something new, a Zig library I'm calling prism. The purpose of this post is to put down in words some of the thoughts I have about how prism should feel to use. While my primary goal is to write something that maybe just I will find useful, since I'd like to reuse prism beyond just seamstress, it would be great to get your feedback (yes yours!) if you have any; feel free to shoot me an email.

Table of Contents

1. The goals

Here are the goals I have for prism.

  1. It should be first and foremost a Zig dependency for use in Zig projects. Since Zig makes it easy to export a C API, it's easy to dream bigger here. But since the main user of prism is me, I don't think starting with this in mind makes sense.
  2. It will be open source.
  3. It should provide a UI framework that runs on macOS, Linux and Windows. Again, it would be easy to dream bigger and attempt to run on phones or WebAssembly; I'm not interested in this, so it's a non-goal.
  4. Because seamstress's look and feel is not “native”, prism should allow customization of the “widgets” it makes use of. Indeed, for example seamstress currently allows defining “buttons” in a perhaps too-complex, but entirely freeform, at-runtime way, and this level of customization of look and feel—even to the level of allowing custom drawing—is a goal.
  5. That being said, prism should take advantage of OS frameworks to the extent that they’re available from Zig. This means that accessing Cocoa’s Objective-C API on macOS is doable but the latest Windows framework is probably not.
  6. I’m not sure there’s an item 6.

2. Layout: putting boxes in boxes

The fundamental building block for a piece of a window’s layout appears to be, for better or worse, the rectangle. I imagine prism as supporting both laying out objects somewhat automatically on a grid with hints to prism as to how to place things, and a more picky, “I want this to be at this pixel” behavior. To this end I’m imagining four fundamental ways of arranging things. These should be nestable more or less arbitrarily, and each one can either contain one (or possibly more) child arrangements, or child “widgets”—so things like buttons, sliders, images, animations, text.

  1. The first two ways enable a grid-style layout. Horizontal arranges its children horizontally. If its children do not take up the full horizontal width, the user should be able to specify the alignment of the children (left, right, center) and if some children do not take up the full height of the container, the user can specify their alignment (top, bottom, center). Spacing between children is configurable in pixels. Children of a Horizontal element are arranged in a row, horizontally.
  2. Vertical is the same, but children of a Vertical element are arranged in a column, vertically.
  3. Box, unlike Horizontal and Vertical, contains only a single child, but allows specifying its location. I imagine Box to be useful when you need granular control over the placement of an object. The dimensions of a Box should be specified.
  4. If only containing one thing feels a little limiting, I think the last primitive, Stack, should be useful. It allows stacking multiple children into the same rectangle. I suppose the size of this rectangle had better be specified by the Stack.

All of these size specifications, if not otherwise, uhh, specified, should be either in pixels or as percentages. Since Horizontal and Vertical have margins, these percentages are of the remaining relevant dimension.

3. Example

Here is a sample layout, together with some Zig code. (At the time of this writing, the code won’t work! Prism is a work in progress!) The point is to illustrate how one might set up such a layout.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// LayoutConfig provides `create` below with configuration options.
pub const LayoutConfig = union(enum) {
    pub const Amount = union(enum) {
        pixels: f64,
        fraction: f64,
    };
    
    Horizontal: struct {
        width: Amount,
        height: Amount,
        spacing_pixels: f64,
        align_contents: enum { left, right, center } = .center,
        align_children: enum { top, bottom, center } = .center,
    },
    Vertical: struct {
        width: Amount,
        height: Amount,
        spacing_pixels: f64,
        align_contents: enum { top, bottom, center } = .center,
        align_children: enum { left, right, center } = .center,
    },
    Box: struct {
        width: Amount,
        height: Amount,
        child_origin: struct {
            x: Amount,
            y: Amount,
        },
    },
    Stack: struct {
        width: Amount,
        height: Amount,
    },
};
// Here is the function signature I had in mind.
pub fn create(config: LayoutConfig, children: anytype) !Layout;

// And finally, here is the above example as a bunch of calls to `create`:
const example_layout = try create(
    // to put the thin column next to the column of rows,
    // we use a Horizontal layout
    .{ .Horizontal = .{
        .width = .{ .fraction = 1.0 },
        .height = .{ .fraction = 1.0 },
        .spacing_pixels = 5.0,
        // .align_contents = .center
        // the above is unnecessary because we provided a default value.
        .align_children = .top,
    } },
    .{
        // we give children as a tuple of Layouts
        // first the lefthand column
        try create(
            .{ .Vertical = .{
                .width = .{ .fraction = 0.3 },
                .height = .{ .fraction = 1.0 },
                .spacing_pixels = 5.0,
                .align_contents = .top,
            } },
            .{
                // children, like the squiggles suggesting text,
                // would go here!
            },
        ),
        // next the righthand column of rows
        try create(
            .{ .Vertical = .{
                .width = .{ .fraction = 0.7 },
                .height = .{ .fraction = 1.0 },
                .spacing_pixels = 3.0,
            } },
            .{
                // there are three children,
                // two of which are straightforward
                try create(.{ .Horizontal = .{
                    .width = .{ .fraction = 1.0 },
                    .height = .{ .pixels = 100 },
                    .spacing_pixels = 10.0,
                    } }, .{
                    // the shapes go here...
                    },
                ),
                try create(.{ .Horizontal = .{
                    .width = .{ .fraction = 1.0 },
                    .height = .{ .pixels = 100 },
                    .spacing_pixels = 5,
                    } }, .{
                    // the text goes here...
                    },
                ),
                // in this call, the height parameter uses up all the
                // remaining height
                try create(.{ .Stack = .{
                    .width = .{ .fraction = 1.0 },
                    .height = .{ .fraction = 1.0 },
                } }, .{
                    // these children will be stacked on top of each other
                    // from back to front
                    try create(.{ .Box = .{
                        .width = .{ .fraction = 1.0 },
                        .height = .{ .fraction = 1.0 },
                        .child_origin = .{
                            .x = .{ .pixels = 0 },
                            .y = .{ .pixels = 0 },
                        },
                    } }, .{
                    // the image goes here...
                    }),
                    try create(.{ .Vertical = .{
                        .width = .{ .fraction = 0.3 },
                        .height = .{ .fraction = 1.0 },
                        .spacing_pixels = 3.0,
                    } }, .{
                    // the circles go here...
                    }),
                    try create(.{ .Box = .{
                        .width = .{ .fraction = 1.0 },
                        .height = .{ .fraction = 1.0 },
                        .child_origin = .{
                            .x = .{ .fraction = 0.7 },
                            .y = .{ .fraction = 0.6 },
                        },
                    } }, .{
                    // the red rotated rectangle goes here...
                    }),
                }),
            },
        ),
    },
);