How do I design a windowing library correctly?

I’m semi new-ish to Zig, I’ve made a handful of simple projects, but now I want to make a larger project, which is to make a (very simple) game completely from scratch, (for fun/learning, not practicality)

But I also noticed that there really aren’t any Zig windowing libraries. Well, there are, but not any that have been updated at all in the last 2 years…
(I’m aware of C windowing libraries, but dont like sacrificing zig’s much nicer error handling etc lol)

So I want to, after finishing this project, separate out the windowing component and adapt it into a library to hopefully be put on github

The problem is, I’m not entirely sure how to design an easy to use, user-friendly library as I have basically no experience in developing libraries, so I wanted to ask for help or suggestions for not only how to design the library but also where to look for more info

Hello @Pixon, welcome to ziggit! :slight_smile:

I am aware of two zig UI projects:

  • Capy - Create cross-platform apps in Zig
  • Mach - Zig game engine & graphics toolkit

Trying to use and/or read the source code of these libraries might give you more ideas.

3 Likes

Thanks!
I already dug into Capy’s source code before due to curiosity, but looked again some more, and I just had a look at Mach’s
I’m just struggling to gain applicable knowledge due to them both not interacting with x11/wayland, directly (instead using stuff like xlib, glfw, (im confused about Mach seemingly using both xlib/wayland-equivelant and also glfw?) and gtk), also due to that im bad at reading other people’s code hehe
But I did see 1 or 2 useful things for what would be necessary for my future library’s features!

1 Like

My understanding is that windowing is a hard problem, as the differences between platforms are large. As is, you do want to have a library dedicated to windowing, as it does abstract away a bunch of complexity.

I would take a look at what wininit-rs does. I have next to none direct experience with this stuff, but, indirectly, it seems that Rust wininit successfully withstood the test of time, so taking its lessons learned might be a good first step.

1 Like

In particular, this lightning talk about winit might be helpful. TL;DW: design your API as

fn callback(window: *Window, event: Event) {
    // ...
}

var w = Window.init();
w.run(callback);

rather than

var w = Window.init();
for (w.nextEvent()) |event| {
    // ...
}
4 Likes

Oh hi @lunacookies, glad to see you here!

1 Like

I’ve been working on one (using libwayland) for the past couple months, haven’t published it yet due to embarrassingly bad code but it’s starting to come together. I’ll post it to Ziggit whenever I consider it “good enough to be considered alpha”

First, what are your goals? Do you want a “native” toolkit, like WxWidgets, Capy or something designed from the ground-up, like RayUi or Nuklear?

Regardless of what you do, it’ll be a lot of work, but building one from the ground up is definitely more difficult due to having to implement EVERYTHING like scrolling, drag and drop, text highlighting, tooltips, etc. Assuming this is what you want to do, I’d highly recommend starting by building your UI kit around something like SDL, then you can make a raw Wayland/X11 backend after you feel your API is decent. I’m actually on my 2nd re-write because I didn’t enjoy using my previous APIs. SDL also allows for easy cross-platform support without needing to write a bunch of backends, which will help you while messing with different APIs, seeing what feels the best.

Before starting, I was in the same boat, scouring for examples and starting points, but ultimately I think the best thing is to just start typing. Create a simple interface to draw a rectangle, that will be your first button. Next, find out how to get mouse input from your backend, do a tiny bit of math to find out if the mouse is hovering, get click input and give your button struct a callback that it can call upon being clicked. You now have your first widget! Now, figure out how to draw some text, either using an existing renderer or writing a simple loader for something like PSF (which is what I did), add label functionality to your button, then start making more complex widgets.

You’ll start to see the shortcomings of your API, which will need to be refactored, but don’t give up, every headache is one step closer to a breakthrough, and every breakthrough you have will get you one step further to your goal. Good luck!

2 Likes

thanks, I tried to look at that, but am having a hard time understanding it due to not understanding rust very well (or, at all), I tried looking at the examples and got quite confused about what was actually being done

I’m just unsure about this as I’m worried it’ll be a pain to, for example modify a local variable, via callbacks, which, correct me if im wrong, isn’t an issue for rust due to lambdas? I’m not entirely sure though, perhaps there’s a way to build an application that completely avoids that issue? clarification/further explanation would be appreciated!

I’m not actually trying to design a UI toolkit, just a lightweight, low resource usage library that can be used to open & use windows, and attach a graphics context such as OpenGL to start rendering whatever, without having to worry much about platform specifics

that is a good point, also thanks! I want to do a bit more research though to hopefully minimise refactors, if possible

The most common library for this kind of thing is GLFW. I believe that the Mach engine maintains Zig GLFW bindings, or you could write your own Zig library around GLFW.

If you don’t want to use GLFW, you could look at writing a Zig windowing library using only the platform specific C libraries: libwayland, xcb and/or xlib for Linux, and the corresponding OS window/input APIs for other platforms. Of course you would still be dependent on the platform specific C libraries, so you could then look at e.g. writing a Zig implementation of the Wayland client protocol. It really comes down to what your goals are: personal education, improving something from what is available, simplifying dependencies, etc.

I am not a frequent Rust user, but I believe that winit is doing something along the lines of what I’ve described above: platform specific C libraries are used where necessary, Rust implementations where possible. E.g. for X11 it uses bindings to the C library, but for Wayland there is a Rust implementation.

I’ve only recently become interested in working on this stuff, so someone more informed might have corrections.

2 Likes

My goal, mainly for learning purposes, is to directly use the X11/Wayland protocol, without xlib/xcb/libwayland/etc, and to hopefully in the end create a convenient and performant end result, if I can manage (and then to use that for a simple project)

But the main thing I meant to ask about was actually what the user of the library would get; as in, the functions, features, setup, and usage, since I have little to no experience with creating libraries and don’t want to make one that’s awful to use!

@marler8997 talk is about X11 and API design:

4 Likes

Personally a few time I found that an iterator API is better compared to callbacks.

An example is parsing XML: expat callback API vs go xml parser.

Maybe for windowing is different?

It was published ~half a year ago here on the forum, I posted some comment, but there was no reaction. :frowning:

Thanks, I’ll take a look at that soon

Mach started off using GLFW, and they’ve recently started replacing GLFW with their own code to interact with the native windowing systems. They’ve started with wayland/x11, but still have GLFW for other platforms.

At that pointer you’re not just designing a windowing library; you’re completely re-implementing xlib & libwayland-client. Most windowing libraries like winit and the like don’t concern themselves with that, and instead just rely on the already-installed libraries, if they exist.

1 Like

Yes, Rust has anonymous functions which can capture their environment (AKA lambdas, closures, etc), while Zig doesn’t even have anonymous functions! So, how would you pass state between main and the callback? There’s several ways you could do it, but probably the most general is to declare a container-level variable which you access from the callback:

const windowing = @import("windowing");

pub fn main() void {
    var w = windowing.Window.init();
    w.run(callback);
}

const Context = struct {
    counter: i32 = 0,
};

var context = Context{};

fn callback(window: *windowing.Window, event: windowing.Event) void {
    switch (event) {
        .Redraw => {
            context.counter += 1;
            // draw counter to the screen ...
        },
        // somehow handle other types of events
    }
}
2 Likes

I think I would prefer something more like this:

const std = @import("std");
// const windowing = @import("windowing");
pub const windowing = struct {
    pub const Event = enum {
        Redraw,
    };

    pub const DrawingContext = struct {
        // ...
    };

    pub fn Window(comptime Context: type) type {
        return struct {
            const Self = @This();

            context: Context,
            dc: DrawingContext = .{},

            pub fn init(context: Context) Self {
                return .{ .context = context };
            }

            pub fn run(self: *Self) void {
                // do some stuff

                self.context.handle(self, .Redraw);
            }
        };
    }

    pub fn window(context: anytype) Window(@TypeOf(context)) {
        return Window(@TypeOf(context)).init(context);
    }

    pub fn run(context: anytype) void {
        var w = window(context);
        w.run();
    }
};

pub fn main() void {
    var context = MyContext{};
    // var w = windowing.window(&context);
    // w.run();
    windowing.run(&context);
}

const MyContext = struct {
    counter: i32 = 0,

    fn handle(self: *MyContext, window: anytype, event: windowing.Event) void {
        switch (event) {
            .Redraw => {
                self.counter += 1;
                // draw counter to the screen ...
                std.debug.print("draw context: {}\n", .{window.dc});
            },
            // somehow handle other types of events
        }
    }
};

That way you still could potentially have multiple different windows with different contexts, swap out contexts per window, for example have a stack of contexts to enter an “application” within an “application” etc., switch between different mini games etc.

1 Like

But I also wonder whether I would be happiest if I could just get an interface of N streams of events for N windows and some way to get a opengl context and use it (without running into thread unsafety because some other thread is trying to use some other opengl window (and it uses some global unique instance for some reason)).

Having to squeeze your application code into a bunch of callbacks can be pretty annoying, often it would be easier if you could just react to the event stream yourself.
And build up your application state from there.
Then it would be cool if there were pre-built utilities to keep track of keyboard states like, whether a button is pressed, so that not everyone re-implements those themselves, but those would just be attached to the event stream and optional.

But I don’t really know a lot about the bad things about platform specific apis, I suspect that many of them give you a bad interface, that doesn’t easily give you the raw data in a simple way.
And consolidating events from different platforms into one similar event probably always requires building a bunch of internal state which effectively is used to rewrite platform specific event streams into platform abstracted events as a new stream.

And I prefer being able to pull those events from a queue, I think the model of “have callbacks for the 5 events I am interested in” could just be another optional thing you can attach to the event stream and it would be used for simple desktop applications that rarely need to redraw or animate things.

While games can just directly process the events if they need that to achieve the best results.

1 Like

I see, interesting

I understand, but I’m doing this acknowledging the lack of practicality
I’m mostly interested in learning about for example how xlib/libwayland, and platform-independent windowing libraries work, and the windowing library is really a side product of this, but I wanted to hopefully make it correctly
As a bonus this would give me a bit of experience in library development too, which isn’t a bad thing!

Oh, now I understand! thanks!

This could work, it sort of groups together the data that could be modified by the events with the actual event handling, which is nice

I kind of agree with this but I’ll have to see as I work on this project!

would this kind of setup work?

const std = @import("std");
//const windowing = @import("windowing.zig");
const windowing = struct {
    pub const ScreenPos = struct {
        x: u16,
        y: u16,
    };
    pub const Event = union(enum) {
        /// x/y = width/height
        resize: ScreenPos,
    };

    /// connects to window server, sets stuff up
    pub fn init() !void {
        // imaginery implementation
    }

    // too lazy to also add an option to enable/disable checking for specific types of events,
    // so imagine that as actually being present here in the example, as it would be really

    pub const Window = struct {
        window_id: u32,
        // etc
        /// returns the next event in queue, otherwise null
        pub fn nextEvent(self: *Window) ?Event {
            // black magic goes here
            _ = self;
            return null;
        }
        /// consumes all events, invoking callback function within context every time
        pub fn consumeAllEvents(self: *Window, context: anytype) anyerror!void {
            while (self.nextEvent()) |e| try context.callback(self, e);
        }
        /// consumes next event if it exists, invoking callback function within context
        pub fn consumeNextEvent(self: *Window, context: anytype) anyerror!void {
            if (self.nextEvent()) |e| try context.callback(self, e);
        }
        /// waits for an event, then consumes all events
        pub fn waitForEvents(self: *Window, context: anytype) anyerror!void {
            // implementation not implemented
            _ = self;
            _ = context;
        }
        /// same as waitForEvents, but doesn't block,
        /// and continues to check for events (until one is found) after returning
        pub fn waitForEventsNoBlock(self: *Window, context: anytype) anyerror!void {
            // implementation not implemented
            _ = self;
            _ = context;
        }
        /// returns true if there are events in queue, otherwise false
        pub fn checkEvents(self: *Window) bool {
            // pretend its implemented
            _ = self;
            return false;
        }
        /// opens a new window and returns it
        pub fn open() !Window {
            // imaginary implementation
            return Window{ .window_id = 123 };
        }
    };
};

// this var wouldnt exist in a real application
const use_iterator = false;

pub fn main() !void {
    // init, open window
    try windowing.init();
    var win = try windowing.Window.open();

    if (use_iterator) { // iterator version
        var width: u16 = 128;
        var height: u16 = 64;

        // main program loop
        while (true) {
            while (win.nextEvent()) |event| switch (event) {
                .resize => |sp| {
                    width = sp.x;
                    height = sp.y;
                },
            };
            // do stuff
        }
    } else { // callback version
        var context = MyContext{};
        // main program loop
        while (true) {
            try win.consumeAllEvents(&context);
            // do stuff
        }
    }
}

const MyContext = struct {
    width: u16 = 128,
    height: u16 = 64,
    pub fn callback(self: *MyContext, win: *windowing.Window, event: windowing.Event) !void {
        _ = win;
        switch (event) {
            .resize => |sp| {
                self.width = sp.x;
                self.height = sp.y;
            },
        }
    }
};

it has an iterator version, and a callback version, the callback version supports 2 options for waiting for events, so that desktop applications can reduce CPU usage (theoretically even with multiple windows?), and an option to check if there are events in the queue without blocking or consuming them for possibly custom logic?

1 Like

For wayland there is a good guide from Drew DeVault: https://wayland-book.com/.

The book uses wayland-client. For a from scratch approach:
https://gaultier.github.io/blog/wayland_from_scratch.html

2 Likes