Another render agnostic immedate mode ui layout libary
Shoots is another render agnostic immedate mode ui layout libary but this time written in zig. you can check out examples for how the libary works, but here are the basic principles:
everything is a node:
const root = UI.Element(.{
.name = "button",
.rect = .{
.h = 20,
.w = 60,
},
.on_click = .{
.func = &updateButton,
.data = &.{},
},
.on_hover = .{
.func = &hoverButton,
.data = &.{},
},
.style = .{
.background_colour = .{
.g = 255,
},
.rounded = 25,
},
},
.children = &[_]UI.Node{
UI.Txt(
UI.Text.init(
"hello world",
try rl.getFontDefault(),
.{},
),
),
UI.Txt(
UI.Text.init(
"hello world again",
try rl.getFontDefault(),
.{},
).setBackground(.{
.a = 255,
.g = 255,
}),
),
},
});
there are 3 differnt types of nodes
- elements these are basically your html divs
- text text is currently just one line
- textures all the fun images you want to include
once you have your node tree setup you should create an arena allocator, since on each render a clone of the node tree will be created this is to ensure that its a forgetful ui system.
var al = std.heap.ArenaAllocator.init(std.heap.smp_allocator);
now using the arena you can either use:
var sized = try UI.resolveSizing(al.allocator(), root);
OR
// clone the root node
var new_tree: Node = try UI.deepClone(root, alloc);
// this is needed as a place holder for the root node
var base_node: Node = Node{ .element = Ele{
.children = &[_]Node{
root,
},
} };
// go through and computes the sizes of all the child elements
computeSizes(&new_tree, &base_node);
// grows the child elements
computeGrowElements(&new_tree);
// computes the pos
computePositions(&new_tree);
This will clone the tree, compute the sizes of the rectangles compute, grow all the child elements, and lastly compute all the positions of the nodes.
next check for any interactions that may occur, these are things like on hover and on click:
const needs_redrawn = UI.processInteractions(&sized, .{
.pos = .{
.x = mouse_pos.x,
.y = mouse_pos.y,
},
.left = rl.isMouseButtonDown(.left),
.middle = rl.isMouseButtonDown(.middle),
.right = rl.isMouseButtonDown(.right),
});
this function returns a bool to determin if the current tree needs to be redrawn, this should be useful in some case.
and finally you can generate the render commands, these commands tell your render where to draw the rectangles on the screen.
const commands = try UI.getRenderCommands(sized, al.allocator());
you can checkout the raylib example for how to parse these render commands
and finally free the arena with a retained_capacity as the ui will more then likely be the exact same size and should only require some few addtional allocs.
_ = al.reset(.retain_capacity);
react like componets:
you can also create react like componets by creating functions blocks that return a Node, please note that this function is inlined, this is required as children from this node will be shared between all compoents that use this node.
inline fn button() UI.Node {
return UI.ElementWborder(
.{
.bottom = 2,
.left = 2,
.right = 2,
.top = 2,
},
.{
.b = 30,
},
.{
.name = "button",
.rect = .{
.h = 20,
.w = 60,
},
.on_click = .{
.func = &updateButton,
.data = &.{},
},
.on_hover = .{
.func = &hoverButton,
.data = &.{},
},
.style = .{
.background_colour = .{
.g = 255,
},
.rounded = 25,
},
},
);
}
adding shoots to your project
fetch the repo
> zig fetch --save git+https://git.sirlilpanda.studio/sirlilpanda/shoots
add to build.zig
const shoots = b.dependency("shoots", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("shoots", shoots.module("shoots"));
resources if you want to either learn out Immediate-Mode ui or make your own:
-
How Clay's UI Layout Algorithm Works really good for the layout algrothim
-
Immediate-Mode Graphical User Interfaces - 2005 good video on explaining the concepts behind immediate mode guis
-
how-to-write-a-flexbox-layout-engine perfect for getting an overview of how all the componets togeather
-
microui some further inporation for how to create ui frameworks