zss
zss is a CSS layout engine and document renderer, written in Zig.
Project structure
- source/values
- Contains CSS value and property definitions.
- source/layout
- The main file here is layout.zig. It does all the heavy lifting of CSS document layout.
- source/render
- Allows one to draw a document to graphical window via a rendering backend. At the moment, the only provided backend is SDL2.
Building zss
To build zss, use the latest version of zig.
To do layout or run tests, you will need:
- harfbuzz
- freetype
To use the SDL2 rendering backend or run the demo program, you must also have:
- sdl2
- sdl2-image
So on Debian, for example, you can do
sudo apt install libharfbuzz-dev libfreetype6-dev libsdl2-dev libsdl2-image-dev
Windows users must provide their own builds of these libraries for now.
After you've installed the dependencies, you can then run zig build --help
to see your options.
How to use zss
Assuming you've imported the library like this...
const zss = @import("zss");
...then the basic workflow is as follows:
- Create a
zss.ElementTree
structure. - Create a
zss.CascadedValueStore
structure. - Call
zss.layout.doLayout
to receive azss.used_values.BoxTree
. - Draw the resulting box tree using the SDL2 rendering backend by calling
zss.render.sdl.drawBoxTree
.
A demo program is provided to show how one could use zss. To see it, run zig build
then zig-out/bin/demo
.
When you run it, make sure you are in the project root directory.
ElementTree
Using Layout is the process of taking a tree of elements, with each element being associated with a set of CSS values, and producing the information necessary to draw them to the screen. In zss, creating these elements is done using ElementTree
, and associating them with values is done using CascadedValueStore
, which is covered in the next section.
Here is some example usage of the ElementTree
.
var tree = zss.ElementTree{};
defer tree.deinit(allocator); // Free the memory used by this tree.
try tree.ensureTotalCapacity(allocator, 100); // Have enough memory for all the elements we want to create.
// The first element must be made with 'createRootAssumeCapacity'.
// If this ElementTree were representing an HTML document, this would be like the <html> or <body> element.
const root = tree.createRootAssumeCapacity();
// Subsequent elements can be created with 'appendChildAssumeCapacity',
// passing the parent element as an argument.
const first_child = tree.appendChildAssumeCapacity(root);
const second_child = tree.appendChildAssumeCapacity(root);
const grandchild = tree.appendChildAssumeCapacity(first_child);
The functions createRootAssumeCapacity
and appendChildAssumeCapacity
return zss.ElementRef
, which is a handle that can be used to refer to the new element. This will be useful for associating CSS values to that element.
CascadedValueStore
Using The CascadedValueStore
maps each element in the ElementTree
to its cascaded values. The store contains many individual maps, whose keys are zss.ElementRef
and values are a set of related CSS properties. You may look at source/layout/CascadedValueStore.zig to see exactly which maps correspond to which properties.
Here are some short guides on using CascadedValueStore
.
Specifying widths and heights
The content_width
and content_height
fields can be used to set the size of content boxes, and the horizontal_edges
and vertical_edges
can be used to set the sizes of margin boxes, border boxes, and padding boxes.
// Creating our CascadedValueStore.
var cvs = CascadedValueStore{};
defer cvs.deinit(allocator);
try cvs.ensureTotalCapacity(allocator, 100);
// Assuming `my_element` was a ElementRef previously returned from an ElementTree.
cvs.content_width.setAssumeCapacity(my_element, .{ .size = .{ .percentage = 0.7 } });
cvs.horizontal_edges.setAssumeCapacity(my_element, .{ .margin_start = .{ .px = 20 } });
cvs.content_height.setAssumeCapacity(my_element, .{ .max_size = .none });
cvs.vertical_edges.setAssumeCapacity(my_element, .{ .border_end = .{ .px = 50 }, .margin_start = .auto });
In this code, we can see that each map takes in a value that corresponds to a certain set of CSS properties. We also see that multiple properties can be set within one line of code. When setting a property, you can write in as many or as few of the struct fields as you want. Any fields you leave out will be assigned the value .undeclared
.
The above code would be roughly equivalent to the following CSS:
#my_element {
width: 70%;
margin-left: 20px;
max-height: none;
border-bottom-width: 50px;
margin-top: auto;
}
Note that within horizontal_edges
, setting margin_start
affects the 'margin-left' property, while within vertical_edges
, it affects the 'margin-top' property. This is because both maps use the same value type, and the meaning of each field varies depending on what map is being used. In this case, "start/end" are in reference to the typical CSS usage of these words, where they mean "left/right" in some contexts and "top/bottom" in others. It is important to read source/layout/CascadedValueStore.zig so that you always know which property a certain field corresponds to.
Adding text
To add text to a document, you must create a "text node" in your element tree. A text node is any node which has its display
property set to .text
. Once you've done that, you can set its text
property to the string it should contain. At the moment, only Latin characters are supported, but more support for Unicode is planned.
Example:
cvs.display.setAssumeCapacity(text_node, .{ .display = .text });
cvs.text.setAssumeCapacity(text_node, .{ .text = "Hello world!" });
Note that a text node must be a leaf node, i.e. it cannot have any children in the element tree.
Fonts
Due to limitations in zss at the moment, it is only possible to define one font and font color for your entire document. This will be the case until more robust font handling is implemented.
Fonts are specified using Harfbuzz's hb_font_t
. Once you have one of those, you may set the font
and color
properties on the root element to customize the font.
const hb_font = hb_font_create(...);
// `root` must be the root element of the ElementTree.
cvs.font.setAssumeCapacity(root, .{ .font = .{ .font = hb_font } });
cvs.color.setAssumeCapacity(root, .{ .color = .{ .rgba 0x336699ff } });
The 'all' property
The 'all' property in CSS resets all of the properties of an element to one of a few keywords. In zss, all
sets the values of all of the undeclared properties of an element. A property is undeclared if it is set to .undeclared
, or if its entry in the CascadedValueStore
does not exist.
Adding background images
zss supports background images, but does not care about the representation in memory of the image. Instead, background images are handled using an interface, whose name is zss.values.BackgroundImage.Object
. By using interfaces, zss is not tied to a particular rendering backend, and can easily extract just the information about an image that it needs.
For example, when using the SDL2 rendering backend, you can use zss.render.sdl.textureAsBackgroundImageObject
to wrap a SDL_Texture
into an Object
.
const texture = SDL_CreateTexture(...);
const bg_img_object = textureAsBackgroundImageObject(texture);
cvs.background2.setAssumeCapacity(my_element, .{ .image = .{ .object = bg_img_object } });
Other background-related properties can be set using the background1
and background2
fields of CascadedValueStore
.
License
GPL-3.0-only
Copyright (C) 2020-2022 Chadwain Holness
zss is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this library. If not, see https://www.gnu.org/licenses/.