summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSteven Van Dorp <steven@vandorp.lu>2026-02-06 10:00:08 +0100
committerSteven Van Dorp <steven@vandorp.lu>2026-02-06 10:00:08 +0100
commit0d8aeb6464103ec8e35c10c448bf08b0b9edc156 (patch)
tree80ab47bf3e0f137856d8494753e774c8456c85c8 /src
Initial CommitHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/app/app.zig126
-rw-r--r--src/app/input.zig281
-rw-r--r--src/app/ui.zig1054
-rw-r--r--src/linux.zig319
-rw-r--r--src/x11.zig331
5 files changed, 2111 insertions, 0 deletions
diff --git a/src/app/app.zig b/src/app/app.zig
new file mode 100644
index 0000000..1f33960
--- /dev/null
+++ b/src/app/app.zig
@@ -0,0 +1,126 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const mem = std.mem;
+const fmt = std.fmt;
+const fs = std.fs;
+const io = std.io;
+
+const Allocator = mem.Allocator;
+const Thread = std.Thread;
+
+const input = @import("input.zig");
+const gui = @import("ui.zig");
+
+var allocator: Allocator = undefined;
+
+pub const Pixel = packed struct {
+ r: u8 = 0,
+ g: u8 = 0,
+ b: u8 = 0,
+ _a: u8 = undefined,
+};
+var pixel_buffer: *const []Pixel = undefined;
+
+pub const Action = enum {
+ primary,
+ text_cursor_left,
+ text_cursor_right,
+};
+
+// DEBUG
+var img: gui.Image = undefined;
+var font: gui.Font = undefined;
+
+pub fn init() !void {
+ allocator = @import("root").allocator;
+
+ img = try .fromPath("assets/img/test_image.png", allocator);
+ //font = try .fromPath("assets/font/OpendyslexicRegular.otf", allocator, .{.font_height = 128});
+ font = try .fromPath("assets/font/OpenSans-VariableFont_wdth,wght.ttf", allocator, .{.font_height = 256});
+
+ try input.bindPointerButton(.primary, .left);
+ try input.bindKey(.text_cursor_left, .arrow_left);
+ try input.bindKey(.text_cursor_right, .arrow_right);
+
+ try gui.globalInit(allocator);
+ ui = try gui.Context.init(.{.allocator = allocator});
+ errdefer ui.deinit();
+
+ user_input = try ui.textBufferFromString("samplwwwawwwwwwwwwwwwwwwwwwwwwwwwwwwww");
+}
+
+pub fn deinit() void {
+ font.deinit();
+ img.deinit();
+
+ user_input.deinit();
+ ui.deinit();
+ gui.globalDeinit();
+}
+
+var window_size: @Vector(2, u32) = undefined;
+var fwindow_size: @Vector(2, f32) = undefined;
+pub fn setWindowSize(ws: @Vector(2, c_uint)) void {
+ window_size = @intCast(ws);
+ fwindow_size = @floatFromInt(window_size);
+ pixel_buffer = @ptrCast(@import("root").pixels);
+}
+
+var ui: gui.Context = undefined;
+var user_input: gui.TextBuffer = undefined;
+var dbg_poss = [3]gui.V2{.{0.1, 0.1}, .{0.2, 0.2}, .{0.3, 0.3}};
+var dbg_colors = [3]gui.Color{.hex(0xFFFF0000), .hex(0xFF00FF00), .hex(0xFF0000FF)};
+var time: f32 = 0;
+pub fn loop(dt: f32) !bool {
+ // TODO styles or something. TBD once we have some real world tests
+ ui.frameStart();
+ //std.debug.print("{d}\n", .{gui.mouse_delta});
+ //if (@reduce(.And, gui.mouse_delta == @Vector(2, f32){0, 0})) return false;
+ time += dt;
+ try ui.textFmt("{d:.1} FPS", .{1/dt}, .{.pos = .{0, 0}, .size = .{1, 1}}, .{.font = &font, .scale = 0.1});
+ //std.debug.print("{d:.1}\n", .{1/dt});
+
+ //const pos = gui.V2{-1+time*0.1,-1+time*0.1};
+ //const color = gui.Color.float(1,1,0,0.5);
+ //try ui.rectangle(.{.pos = pos, .size = .{1,1}}, .{.color = color});
+ try ui.rectangle(.{.pos = .{0,0}, .size = .{1,1}}, .{.color = .float(1,1,1,1), .image = img});
+
+ //try ui.areaBegin(.{.rect = .{.pos = .{time * 0.0, 0.0}, .size = .{0.9-0.0*time, 0.9}}, .children_offset = .{time * 0.0, -time * 0.1}});
+ //for (0..1) |i| {
+ // try ui.textField(&user_input, @import("root").utf8_of_keys_pressed, .{.pos = .{0, 0.1 + 0.1 * @as(f32, @floatFromInt(i)) / 3}, .size = .{1, 1}}, .{
+ // .text_options = .{.font = &font, .scale = 0.05},
+ // .cursor_to_hovered_glpyh_if = input.ended(.primary),
+ // .cursor_move_left = input.ended(.text_cursor_left),
+ // .cursor_move_right = input.ended(.text_cursor_right),
+ // });
+ //}
+ //try ui.rectangle(.{.pos = .{time * 0.0, 0}, .size = .{1,1}}, .{
+ // .color = .hex(0xffff0000),
+ //});
+ //try ui.rectangle(ui._current_area.rect, .{.color = .hex(0xff00ff00), .apply_area_transformations = false, .clip_to_area = true});
+ //try ui.areaEnd();
+
+ //for (&dbg_poss, &dbg_colors) |*dbg_pos, dbg_color| {
+ // var rect_drag: ?gui.V2 = null;
+ // try ui.rectangle(.{.pos = dbg_pos.*, .size = .{1, 0.2}}, .{
+ // .color = dbg_color,
+ // .drag = &rect_drag,
+ // .begin_drag_if_hover_and = input.started(.primary),
+ // .end_drag_if = input.ended(.primary),
+ // });
+ // if (rect_drag) |drag| {
+ // dbg_pos.* += drag;
+ // dbg_pos[0] = @max(dbg_pos[0], 0);
+ // dbg_pos[1] = @max(dbg_pos[1], 0);
+ // }
+ //}
+
+ gui.clear(@ptrCast(pixel_buffer.*));
+ try ui.draw(.{
+ .pixels = @ptrCast(pixel_buffer.*),
+ .bytes_per_pixel = @sizeOf(Pixel),
+ .pixels_per_row = window_size[0],
+ });
+ return true;
+}
+
diff --git a/src/app/input.zig b/src/app/input.zig
new file mode 100644
index 0000000..d17199b
--- /dev/null
+++ b/src/app/input.zig
@@ -0,0 +1,281 @@
+const std = @import("std");
+
+const root = @import("root");
+
+const ArrayListUnmanaged = std.ArrayListUnmanaged;
+
+pub const Action = @import("app.zig").Action;
+
+pub const DeviceInput = union(enum) {
+ pub const PointerId = u8;
+ pub const GamepadId = u8;
+ pub const DeviceId = u8;
+
+ pub const PointerAxis = enum {x, y, scroll_x, scroll_y, pressure};
+
+ key: struct {
+ code: root.Key
+ },
+ pointer_axis: struct {
+ pointer_id: PointerId = 0,
+ axis: PointerAxis,
+ },
+ pointer_button: struct {
+ pointer_id: PointerId = 0,
+ button: root.Button,
+ },
+ gamepad_axis: struct {
+ gamepad_id: GamepadId = 0,
+ axis: u8,
+ },
+ gamepad_button: struct {
+ gamepad_id: GamepadId = 0,
+ button: root.Button,
+ },
+ gyro_axis: struct {
+ device_id: DeviceId = 0,
+ axis: enum {x, y, z},
+ },
+ accel_axis: struct {
+ device_id: DeviceId = 0,
+ axis: enum {x, y, z},
+ },
+
+ pub fn eql(self: DeviceInput, other: DeviceInput) bool {
+ return std.meta.eql(self, other);
+ }
+};
+
+pub const InputBinding = struct {
+ pub const InputState = struct {
+ input: DeviceInput,
+ state: bool = false,
+ };
+
+ inputs: []InputState,
+ //input: DeviceInput,
+ curve: Curve,
+ action: Action,
+};
+
+pub const State = struct {
+ // TODO it should be possible to initialize this to a non-zero value, maybe to curve.eval(0)
+ current: f32 = 0,
+ previous: f32 = 0,
+
+ pub fn started(self: State) bool {
+ return self.previous < 1 and self.current >= 1;
+ }
+ pub fn active(self: State) bool {
+ return self.current >= 1;
+ }
+ pub fn ended(self: State) bool {
+ return self.current < 1 and self.previous >= 1;
+ }
+ pub fn value(self: State) f32 {
+ return self.current;
+ }
+ pub fn delta(self: State) f32 {
+ return self.previous - self.previous;
+ }
+};
+pub fn started(action: Action) bool {
+ return getState(action).started();
+}
+pub fn active(action: Action) bool {
+ return getState(action).active();
+}
+pub fn ended(action: Action) bool {
+ return getState(action).ended();
+}
+pub fn value(action: Action) f32 {
+ return getState(action).value();
+}
+pub fn delta(action: Action) f32 {
+ return getState(action).delta();
+}
+
+pub var allocator: std.mem.Allocator = undefined;
+
+pub var bindings: ArrayListUnmanaged(InputBinding) = undefined;
+pub var action_states = [_]State{.{}}**std.meta.fields(Action).len;
+
+const InitArgs = struct {
+ allocator: ?std.mem.Allocator = null,
+ initial_binding_capacity: usize = 16,
+};
+pub fn _init(args: InitArgs) !void {
+ allocator = args.allocator orelse root.allocator;
+ bindings = try @TypeOf(bindings).initCapacity(allocator, args.initial_binding_capacity);
+ errdefer bindings.deinit(allocator);
+}
+
+pub fn _deinit() void {
+ for (bindings.items) |*binding| {
+ allocator.free(binding.inputs);
+ if (binding.curve.keys) |keys| allocator.free(keys);
+ }
+ bindings.deinit(allocator);
+}
+
+pub fn _before_poll() void {
+ for (&action_states) |*state| {
+ state.previous = state.current;
+ }
+}
+
+pub fn getState(action: Action) *State {
+ return &action_states[@intFromEnum(action)];
+}
+
+pub fn simulate(input: DeviceInput, raw_value: f32) void {
+ // FIXME if we have Ctrl+A = .foo and A = .bar, then start with A, and later hold Ctrl, both .foo and .bar are active
+ var winner: ?*InputBinding = null;
+ for (bindings.items) |*binding| {
+ var hit = false;
+ var state = true;
+ for (binding.inputs) |*binding_input| {
+ if (binding_input.input.eql(input)) {
+ hit = true;
+ binding_input.state = raw_value != 0;
+ break;
+ }
+ state &= binding_input.state;
+ }
+ if (!hit) continue;
+ if (winner != null and binding.inputs.len <= winner.?.inputs.len) continue;
+ if (!state) continue;
+ winner = binding;
+ }
+
+ if (winner == null) return;
+
+ for (winner.?.inputs) |is| {
+ if (raw_value != 0 and !is.state) return;
+ }
+
+ const action_state = getState(winner.?.action);
+ action_state.*.current = winner.?.curve.eval(raw_value);
+}
+
+pub fn bind(action: Action, input: []const DeviceInput, curve: Curve) !void {
+ const input_states = try allocator.alloc(InputBinding.InputState, input.len);
+ errdefer allocator.free(input_states);
+ for (input_states, input) |*input_state, device_input| {
+ input_state.* = .{
+ .input = device_input,
+ .state = switch (device_input) {.key, .pointer_button => false, else => true},
+ };
+ }
+ try bindings.append(allocator, .{
+ .inputs = input_states,
+ .curve = curve,
+ .action = action,
+ });
+}
+pub fn bindKey(action: Action, key: CommonKey) !void {
+ try bind(action, &.{.{.key = .{.code = root.getPlatformKey(key)}}}, Curve.linear());
+}
+pub fn bindPointerAxis(action: Action, axis: DeviceInput.PointerAxis) !void {
+ try bind(action, &.{.{.pointer_axis = .{.axis = axis}}}, Curve.linear());
+}
+pub fn bindPointerButton(action: Action, button: CommonButton) !void {
+ try bind(action, &.{.{.pointer_button = .{.button = root.getPlatformButton(button)}}}, Curve.linear());
+}
+
+
+
+
+
+pub const Curve = struct {
+ pub const Key = struct {
+ x: f32,
+ y: f32,
+ in_tangent: f32,
+ out_tangent: f32,
+ };
+
+ keys: ?[]Key,
+
+ pub fn init(keys: []Key) !Curve {
+ const var_keys = try allocator.alloc(Key, keys.len);
+ errdefer allocator.free(var_keys);
+ @memcpy(var_keys, keys);
+ return .{
+ .keys = var_keys,
+ };
+ }
+
+ pub fn eval(c: Curve, x: f32) f32 {
+ if (c.keys == null) return x;
+
+ const n = c.keys.?.len;
+ if (n == 0) return 0;
+
+ var i: usize = 0;
+
+ if (x <= c.keys.?[0].x) {
+ i = 0;
+ } else if (x >= c.keys.?[n-1].x) {
+ i = n - 2;
+ } else {
+ while (i+1 < n and x > c.keys.?[i+1].x) : (i += 1) {}
+ }
+
+
+ // https://en.wikipedia.org/wiki/Cubic_Hermite_spline
+
+ const k0 = c.keys.?[i];
+ const k1 = c.keys.?[i+1];
+
+ const t = (x - k0.x) / (k1.x - k0.x);
+ const t2 = t*t;
+ const t3 = t2*t;
+
+ const h00 = 2*t3 - 3*t2 + 1;
+ const h10 = t3 - 2*t2 + t;
+ const h01 = -2*t3 + 3*t2;
+ const h11 = t3 - t2;
+
+ const dx = k1.x - k0.x;
+
+ return
+ h00 * k0.y +
+ h10 * k0.out_tangent * dx +
+ h01 * k1.y +
+ h11 * k1.in_tangent * dx;
+ }
+
+ pub fn linear() Curve {
+ return .{.keys = null};
+ }
+
+ // TODO
+ //pub fn invert(c: *const Curve) *const Curve {
+ // for (c.keys) |*k| {
+ // k.y = 1 - k.y;
+ // }
+ // return c;
+ //}
+
+ //pub fn deadzone(c: *const Curve, min_activation: f32) *const Curve {
+ // for (c.keys) |*k| {
+ // if (k.* >= min_activation) return;
+ // k.* = 0;
+ // }
+ // return c;
+ //}
+};
+
+pub const CommonKey = enum {
+ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
+ left_control, right_control,
+ left_shift, right_shift,
+ arrow_left, arrow_right, arrow_up, arrow_down,
+};
+pub const CommonButton = enum {
+ left,
+ middle,
+ right,
+};
+
diff --git a/src/app/ui.zig b/src/app/ui.zig
new file mode 100644
index 0000000..245ba12
--- /dev/null
+++ b/src/app/ui.zig
@@ -0,0 +1,1054 @@
+const std = @import("std");
+const mem = std.mem;
+const fs = std.fs;
+
+const c = @cImport({
+ @cInclude("stb_image.h");
+ @cInclude("stb_truetype.h");
+});
+
+const Allocator = mem.Allocator;
+const ArrayListUnmanaged = std.ArrayListUnmanaged;
+const AutoHashMapUnmanaged = std.AutoHashMapUnmanaged;
+const Thread = std.Thread;
+
+const assert = std.debug.assert;
+
+pub const V2i = @Vector(2, i32);
+pub const V2u = @Vector(2, u32);
+pub const V2 = @Vector(2, f32);
+pub const V4 = @Vector(4, f32);
+pub const Color = struct {
+ vec: @Vector(4, f32),
+
+ pub fn float(r: f32, g: f32, b: f32, a: f32) Color {
+ return .{.vec = .{b, g, r, a}};
+ }
+
+ pub fn hex(argb: u32) Color {
+ return .{.vec = @as(@Vector(4, f32), @floatFromInt(@as(@Vector(4, u8), @bitCast(mem.nativeToLittle(u32, argb))))) / @as(@Vector(4, f32), @splat(255))};
+ }
+};
+pub const GlyphIndex = i32;
+
+pub var prev_mouse_pos: V2 = V2{0, 0};
+pub var mouse_pos = V2{0, 0};
+pub var mouse_delta = V2{0, 0};
+
+pub fn globalInit(allocator: Allocator) !void {
+ try RenderQueue.init(allocator);
+}
+
+pub fn globalDeinit() void {
+ RenderQueue.deinit();
+}
+
+pub const Rect = struct {
+ pos: V2,
+ size: V2,
+
+ pub const screenSpaceFill = Rect{.pos = .{0, 0}, .size = .{1, 1}};
+
+ pub fn resolve(r: anytype) Rect {
+ return switch (@TypeOf(r)) {
+ Rect => r,
+ ?Rect => r orelse Rect.screenSpaceFill,
+ else => @compileError("Rect.resolve() only accepts Rect and ?Rect. You passed: "++@typeName(@TypeOf(r))),
+ };
+ }
+
+ pub fn contains(r: Rect, v: V2) bool {
+ return @reduce(.And, r.pos <= v) and @reduce(.And, v <= r.pos+r.size);
+ }
+
+ fn intersection(a: Rect, b: Rect) Rect {
+ const a_min = a.pos;
+ const a_max = a.pos + a.size;
+ const b_min = b.pos;
+ const b_max = b.pos + b.size;
+
+ const i_min = @max(a_min, b_min);
+ const i_max = @min(a_max, b_max);
+ const i_size = i_max - i_min;
+
+ if (@reduce(.Or, i_size <= V2{0,0})) {
+ // any component <= 0 means no intersection
+ return .{.pos = .{0,0}, .size = .{0,0}};
+ }
+
+ return .{.pos = i_min, .size = i_size};
+ }
+};
+
+pub const Area = struct {
+ rect: Rect = .{.pos = .{0, 0}, .size = .{1, 1}},
+ children_offset: V2 = .{0, 0},
+
+ pub fn intersection(self: *Area, other: Area) void {
+ self.rect = self.rect.intersection(other.rect);
+ self.children_offset += other.children_offset;
+ }
+};
+
+pub const Command = union(enum) {
+ rect: Rect,
+ color: Color,
+ image: ?Image,
+ image_rect: Rect,
+ text_begin: struct {},
+ text_end: struct {},
+ //area_begin: Area,
+ //area_end: struct {},
+};
+
+pub const CommandBuffer = struct {
+ commands: ArrayListUnmanaged(Command),
+ allocator: Allocator,
+
+ pub fn init(initial_capacity: u16, allocator: Allocator) !CommandBuffer {
+ return .{
+ .commands = try ArrayListUnmanaged(Command).initCapacity(allocator, initial_capacity),
+ .allocator = allocator,
+ };
+ }
+
+ pub fn deinit(cb: *CommandBuffer) void {
+ cb.commands.deinit(cb.allocator);
+ }
+
+ pub fn rect(cb: *CommandBuffer, r: Rect) !void {
+ try cb.commands.append(cb.allocator, .{.rect = r});
+ }
+
+ pub fn color(cb: *CommandBuffer, value: Color) !void {
+ try cb.commands.append(cb.allocator, .{.color = value});
+ }
+
+ pub fn textBegin(cb: *CommandBuffer) !void {
+ try cb.commands.append(cb.allocator, .{.text_begin = .{}});
+ }
+
+ pub fn textEnd(cb: *CommandBuffer) !void {
+ try cb.commands.append(cb.allocator, .{.text_end = .{}});
+ }
+
+ pub fn image(cb: *CommandBuffer, img: ?Image) !void {
+ try cb.commands.append(cb.allocator, .{.image = img});
+ }
+
+ pub fn image_rect(cb: *CommandBuffer, r: Rect) !void {
+ try cb.commands.append(cb.allocator, .{.image_rect = r});
+ }
+
+ //pub fn areaBegin(cb: *CommandBuffer, area: Area) !void {
+ // try cb.commands.append(cb.allocator, .{.area_begin = area});
+ //}
+
+ //pub fn areaEnd(cb: *CommandBuffer) !void {
+ // try cb.commands.append(cb.allocator, .{.area_end = .{}});
+ //}
+};
+
+pub const Image = struct {
+ size: V2,
+ channels: u8,
+ pixels: []Color,
+ allocator: Allocator,
+
+ pub fn fromPath(path: [:0]const u8, allocator: Allocator) !Image {
+ var width: c_int = undefined;
+ var height: c_int = undefined;
+ var channels: c_int = undefined;
+ const desired_channels = 4;
+
+ const c_pixels = c.stbi_loadf(
+ path.ptr,
+ &width,
+ &height,
+ &channels,
+ desired_channels,
+ );
+ if (c_pixels == null) {
+ return error.ImageLoadFail;
+ }
+ defer c.stbi_image_free(c_pixels);
+
+ const pixels = try allocator.alloc(Color, @as(u32, @intCast(width)) * @as(u32, @intCast(height)));
+ for (pixels, 0..) |*p, i| {
+ const src = c_pixels[i*4..i*4+4];
+ p.*.vec = .{src[2], src[1], src[0], src[3]};
+ }
+
+ return .{
+ .pixels = pixels,
+ .size = @floatFromInt(@Vector(2, c_int){width, height}),
+ .channels = desired_channels,
+ .allocator = allocator,
+ };
+ }
+
+ pub fn deinit (img: Image) void {
+ img.allocator.free(img.pixels);
+ }
+};
+
+pub const Font = struct {
+ pub const Glyph = struct {
+ image: Image,
+ offset: V2,
+
+ pub fn deinit(glyph: Glyph) void {
+ glyph.image.deinit();
+ }
+ };
+
+ allocator: Allocator,
+ glyphs: AutoHashMapUnmanaged(u21, Glyph),
+ font_height: f32,
+ // TODO fallback fonts
+
+ raw: [:0]const u8,
+ info: c.stbtt_fontinfo,
+ scale: f32,
+
+ pub const FontLoadArgs = struct {
+ font_height: f32 = 32,
+ line_height_scale: f32 = 1
+ };
+ pub fn fromPath(path: [:0]const u8, allocator: Allocator, args: FontLoadArgs) !Font {
+ var info: c.stbtt_fontinfo = undefined;
+ const raw = try fs.cwd().readFileAllocOptions(allocator, path, std.math.maxInt(usize), null, .@"1", 0);
+ errdefer allocator.free(raw);
+
+ if (c.stbtt_InitFont(&info, raw.ptr, 0) == 0) {
+ return error.FontLoadFail;
+ }
+ //const kerning_table_len = @as(usize, @intCast(c.stbtt_GetKerningTableLength(&info)));
+ //if (kerning_table_len != 0) {
+ // var kerning_table = try allocator.alloc(c.stbtt_kerningentry, kerning_table_len);
+ // defer allocator.free(kerning_table);
+ // _ = c.stbtt_GetKerningTable(&info, &kerning_table[0], @intCast(kerning_table.len));
+ //}
+
+ const scale = c.stbtt_ScaleForPixelHeight(&info, args.font_height);
+ //var ascent: c_int = undefined;
+ //var descent: c_int = undefined;
+ //var line_gap: c_int = undefined;
+ //c.stbtt_GetFontVMetrics(&info, &ascent, &descent, &line_gap);
+ ////const baseline = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(ascent)) * scale));
+ //const line_height = @as(c_int, @intFromFloat(@as(f32, @floatFromInt((ascent - descent + line_gap))) * args.line_height_scale * scale));
+ //std.debug.print("line-gap: {d}\n", .{line_gap});
+
+ var font = Font{
+ .allocator = allocator,
+ .glyphs = .{},
+ .font_height = args.font_height,
+ .raw = raw,
+ .info = info,
+ .scale = scale,
+ };
+ try font.glyphs.ensureUnusedCapacity(allocator, 64);
+ errdefer font.glyphs.deinit(allocator);
+
+ errdefer {
+ var glyphs = font.glyphs.valueIterator();
+ while (glyphs.next()) |glyph| {
+ glyph.deinit();
+ }
+ }
+ return font;
+ }
+
+ pub fn loadGlyph(font: *Font, codepoint: u21) !void {
+ var c_glyph_w: c_int = undefined;
+ var c_glyph_h: c_int = undefined;
+ var c_x_offset: c_int = undefined;
+ var c_y_offset: c_int = undefined;
+ var glyph: Glyph = undefined;
+ const c_bitmap = c.stbtt_GetCodepointBitmap(&font.info, 0, font.scale, codepoint, &c_glyph_w, &c_glyph_h, &c_x_offset, &c_y_offset);
+ defer c.stbtt_FreeBitmap(c_bitmap, null);
+ const glyph_w: u32 = @intCast(c_glyph_w);
+ const glyph_h: u32 = @intCast(c_glyph_h);
+ glyph.image.allocator = font.allocator;
+ glyph.image.pixels = try font.allocator.alloc(Color, glyph_w*glyph_h);
+ errdefer font.allocator.free(glyph.image.pixels);
+ for (0..glyph_w*glyph_h) |j| {
+ const value = c_bitmap[j];
+ const fvalue = @as(f32, @floatFromInt(value)) / 255;
+ glyph.image.pixels[j].vec = .{1,1,1,fvalue};
+ }
+ const font_height_2 = @as(V2, @splat(font.font_height));
+ glyph.image.size = @as(V2, @floatFromInt(V2i{c_glyph_w, c_glyph_h}));
+ glyph.offset = V2{@floatFromInt(c_x_offset), @floatFromInt(c_y_offset)} / font_height_2;
+ try font.glyphs.put(font.allocator, codepoint, glyph);
+ }
+
+ pub fn deinit(font: *Font) void {
+ var glyphs = font.glyphs.valueIterator();
+ while (glyphs.next()) |glyph| {
+ glyph.deinit();
+ }
+ font.glyphs.deinit(font.allocator);
+ font.allocator.free(font.raw);
+ }
+
+ pub fn getGlyph(font: *Font, cp: u21) ?Glyph {
+ if (font.glyphs.get(cp)) |glyph| {
+ return glyph;
+ }
+ font.loadGlyph(cp) catch return null;
+ return font.glyphs.get(cp);
+ }
+};
+
+pub const Context = struct {
+ allocator: Allocator,
+ command_buffer: CommandBuffer,
+ area_stack: ArrayListUnmanaged(Area) = .{},
+ _current_area: Area = .{},
+
+ hover_consumed: bool = false,
+ hover_consumed_last_frame: bool = false,
+
+ dragging: bool = false,
+ dragged_last_frame: bool = false,
+
+ const InitOptions = struct {
+ allocator: Allocator,
+ command_buffer_initial_capacity: u16 = 128,
+ render_target_stack_initial_capacity: u16 = 1,
+ };
+ pub fn init(o: InitOptions) !Context {
+ return Context{
+ .allocator = o.allocator,
+ .command_buffer = try .init(o.command_buffer_initial_capacity, o.allocator),
+ };
+ }
+
+ pub fn deinit(ctx: *Context) void {
+ ctx.command_buffer.deinit();
+ ctx.area_stack.deinit(ctx.allocator);
+ }
+
+ pub fn frameStart(ctx: *Context) void {
+ mouse_delta = mouse_pos - prev_mouse_pos;
+ ctx.command_buffer.commands.clearRetainingCapacity();
+ }
+
+ pub const DrawTarget = struct {
+ pixels: []u8,
+ bytes_per_pixel: u8,
+ pixels_per_row: u32,
+ };
+ pub fn draw(ctx: *Context, target: DrawTarget) !void {
+ // TODO performance: optimize command buffer to minimize overdraw. This could also allow for more efficient multi-threading
+ prev_mouse_pos = mouse_pos;
+ const ftarget_width: f32 = @floatFromInt(target.pixels_per_row);
+ const ftarget_height: f32 = @floatFromInt(target.pixels.len / target.pixels_per_row / target.bytes_per_pixel);
+ const fdimensions = V2{ftarget_width, ftarget_height};
+ const commands = ctx.command_buffer.commands.items;
+ if (commands.len == 0) return;
+ var i: u32 = @intCast(commands.len - 1);
+ var color: Color = undefined;
+ var image: ?Image = null;
+ var img_rect: Rect = undefined;
+ var text_mode = false;
+ while (true) {
+ const command = commands[i];
+ switch (command) {
+ .color => |cc| {
+ color = cc;
+ },
+ .image => |ii| {
+ image = ii;
+ },
+ .image_rect => |r| {
+ img_rect = r;
+ },
+ .rect => |rect| {
+ const scale_factor = @as(V2, @splat(@min(fdimensions[0], fdimensions[1])));
+ //img_rect = .{.pos = .{0,0.5}, .size = .{1,1}};
+ try drawRect(target.pixels, image, img_rect, color, rect.pos * scale_factor, rect.size * scale_factor, target.pixels_per_row, text_mode);
+ },
+ .text_begin => {
+ text_mode = true;
+ RenderQueue.waitUntilAllFinished();
+ },
+ .text_end => {
+ text_mode = false;
+ RenderQueue.waitUntilAllFinished();
+ },
+ }
+
+ if (i == 0) break;
+ i -= 1;
+ }
+ assert(ctx.area_stack.items.len == 0);
+ ctx._current_area = .{};
+
+ ctx.command_buffer.commands.clearRetainingCapacity();
+
+ ctx.hover_consumed_last_frame = ctx.hover_consumed;
+ ctx.hover_consumed = false;
+
+ ctx.dragged_last_frame = ctx.dragging;
+
+ RenderQueue.waitUntilAllFinished();
+ }
+
+ pub const RectangleOptions = struct {
+ color: Color = .hex(0xFFFF_FFFF),
+ image: ?Image = null,
+ //uvs: [4]V2 = .{.{0, 0}, .{1, 0}, .{0, 1}, .{1, 1}},
+ hover: ?*bool = null,
+ consume_hover: bool = true,
+ ignore_hover_if_consumed: bool = true,
+ begin_drag_if_hover_and: bool = false,
+ end_drag_if: bool = false,
+ drag: ?*?V2 = null,
+ apply_area_transformations: bool = true,
+ clip_to_area: bool = true,
+ };
+ pub fn rectangle(ctx: *Context, rect_: Rect, o: RectangleOptions) !void {
+ const current_area_pos = if (o.apply_area_transformations) ctx._current_area.rect.pos else V2{0,0};
+ const current_area_children_offset = if (o.apply_area_transformations) ctx._current_area.children_offset else V2{0,0};
+ const pos = rect_.pos + current_area_pos + current_area_children_offset;
+ const size = rect_.size;
+
+ var rect = Rect{.pos = .{0,0}, .size = .{1,1}};
+ if (o.clip_to_area) {
+ rect = ctx._current_area.rect.intersection(.{.pos = pos, .size = size});
+ }
+ if (@reduce(.Or, rect.size == V2{0,0})) return;
+
+ var img_rect: Rect = .{.pos = .{0,0}, .size = .{1,1}};
+ if (o.clip_to_area) {
+ assert(@reduce(.Or, size != V2{0,0})); // We already checked rect.size, so size itself should never be 0
+ img_rect.pos = @max(V2{0,0}, -pos)/size;
+ img_rect.size = @max(V2{0,0}, @min(V2{1, 1}, V2{1,1} - (pos + size - (ctx._current_area.rect.size))/size)) - img_rect.pos;
+ }
+
+ try ctx.command_buffer.rect(rect);
+ try ctx.command_buffer.image_rect(img_rect);
+ try ctx.command_buffer.image(o.image);
+ try ctx.command_buffer.color(o.color);
+
+ const hovering = rect.contains(mouse_pos);
+ if (o.hover) |h| {
+ h.* = false;
+ }
+ if (!o.ignore_hover_if_consumed or !ctx.hover_consumed) {
+ if (o.hover) |h| {
+ h.* = hovering;
+ if (h.*) {
+ ctx.hover_consumed = true;
+ }
+ }
+ if (o.end_drag_if) {
+ ctx.dragging = false;
+ }
+ const hovered_last_frame = rect.contains(prev_mouse_pos);
+ if (hovered_last_frame) {
+ if (o.begin_drag_if_hover_and and o.drag != null) {
+ ctx.dragging = true;
+ }
+ if (o.drag != null and ctx.dragging) {
+ const d = o.drag.?;
+ // note that we need to check whether we were hovering during the last frame,
+ // because the cursor may have been dragged outside of this rectangle
+ if (hovered_last_frame) {
+ d.* = mouse_delta;
+ ctx.hover_consumed = true;
+ ctx.dragging = true;
+ ctx.dragged_last_frame = true;
+ }
+ }
+ }
+ }
+
+ if (o.consume_hover) {
+ ctx.hover_consumed = ctx.hover_consumed or hovering;
+ }
+ }
+
+ pub fn areaBegin(ctx: *Context, area: Area) !void {
+ try ctx.area_stack.append(ctx.allocator, area);
+ ctx._current_area.intersection(area);
+ }
+
+ pub fn areaEnd(ctx: *Context) !void {
+ _ = ctx.area_stack.pop();
+ ctx._current_area = .{};
+ for (ctx.area_stack.items) |area| {
+ ctx._current_area.intersection(area);
+ }
+ }
+
+ pub const TextWriter = struct {
+ interface: std.Io.Writer,
+ ctx: *Context,
+ font: *Font,
+ scale: V2,
+ rect: Rect,
+ color: Color,
+ hovered_glyph: ?*GlyphIndex,
+ consume_hover: bool,
+ glyph_index: i15 = 0,
+ glyph_offset: V2 = .{0, 0},
+
+ pub fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) !usize {
+ const w: *@This() = @alignCast(@fieldParentPtr("interface", io_w));
+ _ = w.writeAll(io_w.buffer[0..io_w.end]) catch return error.WriteFailed;
+ io_w.end = 0;
+ var acc: usize = 0;
+ for (data) |dat| {
+ if (dat.len == 0) continue;
+ acc += w.writeAll(dat) catch return error.WriteFailed;
+ }
+ if (splat > 0) {
+ for (1..splat) |_| {
+ acc += w.writeAll(data[data.len-1]) catch return error.WriteFailed;
+ }
+ }
+ return acc;
+ }
+
+ pub fn writeAll(w: *TextWriter, bytes: []const u8) !usize {
+ const r = w.rect;
+ var iter = std.unicode.Utf8Iterator{.bytes = bytes, .i = 0};
+ const font_height_2: V2 = @splat(w.font.font_height);
+ while (iter.nextCodepoint()) |cp| : (w.glyph_index += 1) {
+ const codepoint_info = w.font.getGlyph(cp) orelse return error.NoGlyph;
+ const size = codepoint_info.image.size / font_height_2;
+ var hover: bool = undefined;
+
+ try w.ctx.rectangle(
+ .{.pos = r.pos + w.glyph_offset + (V2{0,1}+codepoint_info.offset) * w.scale, .size = size * w.scale}, .{
+ .image = codepoint_info.image,
+ .color = w.color,
+ .hover = if (w.hovered_glyph != null) &hover else null,
+ .consume_hover = w.consume_hover,
+ });
+
+ if (w.hovered_glyph) |hg| {
+ if (hover) {
+ hg.* = w.glyph_index;
+ }
+ }
+
+ // TODO use advanceWidth and leftSideBearing instead of size
+ w.glyph_offset += V2{(size[0] + codepoint_info.offset[0]), 0} * w.scale;
+ }
+
+ return bytes.len;
+ }
+ };
+
+ pub const TextOptions = struct {
+ font: *Font,
+ color: Color = .hex(0xFFFFFFFF),
+ hover: ?*bool = null,
+ consume_hover: bool = true,
+ hovered_glyph: ?*GlyphIndex = null,
+ scale: f32 = 0.05,
+ //TODO align, overflow, rich text
+ };
+ pub fn textFmt(ctx: *Context, comptime fmt: []const u8, args: anytype, rect: Rect, o: TextOptions) !void {
+ try ctx.command_buffer.textEnd();
+
+ var buf = [_]u8{0}**16;
+ const vtable = std.Io.Writer.VTable{
+ .drain = TextWriter.drain,
+ };
+
+ var hovered_glyph: GlyphIndex = -1;
+ var writer = TextWriter{
+ .interface = std.Io.Writer{
+ .buffer = &buf,
+ .vtable = &vtable,
+ },
+ .ctx = ctx,
+ .rect = rect,
+ .font = o.font,
+ .color = o.color,
+ .scale = @as(V2, @splat(o.scale)),
+ .hovered_glyph = &hovered_glyph, // TODO option to ignore for performance, maybe
+ .consume_hover = o.consume_hover,
+ };
+ try writer.interface.print(fmt, args);
+ try writer.interface.flush();
+ if (o.hovered_glyph) |hg| {
+ hg.* = hovered_glyph;
+ }
+
+ if (o.hover) |h| {
+ h.* = hovered_glyph != -1;
+ if (!ctx.hover_consumed) {
+ h.* = h.* or rect.contains(mouse_pos);
+ if (o.consume_hover) {
+ ctx.hover_consumed = ctx.hover_consumed or h.*;
+ }
+ }
+ }
+
+ try ctx.command_buffer.textBegin();
+ }
+
+ pub const TextFieldOptions = struct {
+ text_options: TextOptions,
+ cursor_to_hovered_glpyh_if: bool = false,
+ cursor_move_left: bool = false,
+ cursor_move_right: bool = false,
+ };
+ pub fn textField(ctx: *Context, str: *TextBuffer, to_insert_utf8: []const u8, rect: Rect, o: TextFieldOptions) !void {
+ var text_options = o.text_options;
+
+ var hovered_glyph: GlyphIndex = -1;
+ text_options.hovered_glyph = &hovered_glyph;
+
+ if (str.focus and str.cursor_index >= 0) {
+ const count = try std.unicode.utf8CountCodepoints(to_insert_utf8);
+ if (count > 0) {
+ try str.al.insertSlice(str.allocator, @intCast(str.cursor_index), to_insert_utf8);
+ str.cursor_index += @intCast(to_insert_utf8.len);
+ }
+ }
+
+ try ctx.textFmt("{s}", .{str.toString()}, rect, text_options);
+
+ if (o.cursor_to_hovered_glpyh_if) {
+ str.focus = false;
+ if (hovered_glyph != -1) {
+ str.focus = true;
+ str.cursor_index = hovered_glyph;
+ }
+ }
+ if (o.text_options.hovered_glyph) |hg| {
+ hg.* = hovered_glyph;
+ }
+
+ str.cursor_index = @min(str.cursor_index, str.toString().len);
+ if (str.focus) {
+ if (o.cursor_move_left) {
+ str.cursorMoveLeft();
+ }
+ if (o.cursor_move_right) {
+ str.cursorMoveRight();
+ }
+ if (0 <= str.cursor_index and str.cursor_index < str.toString().len) {
+ const text_commands = ctx.getLastTextCommandSequence();
+ const idx = str.getCodepointIndexAtCursor()*4;
+ if (idx < text_commands.len) {
+ const glyph_rect = text_commands[idx].rect;
+ try ctx.rectangle(glyph_rect, .{
+ .apply_area_transformations = false,
+ .clip_to_area = true,
+ });
+ }
+ }
+ }
+ }
+
+ pub fn getLastTextCommandSequence(ctx: Context) []Command {
+ if (ctx.command_buffer.commands.items.len < 2) return &.{};
+
+ var end: usize = ctx.command_buffer.commands.items.len - 1;
+ while (true) {
+ if (ctx.command_buffer.commands.items[end] == .text_begin) break;
+ if (end == 0) return &.{};
+ end -= 1;
+ }
+
+ var start = end - 1;
+ while (true) {
+ if (ctx.command_buffer.commands.items[start] == .text_end) break;
+ assert(start != 0); // can't happen, since we already found end
+ start -= 1;
+ }
+
+ return ctx.command_buffer.commands.items[start+1..end];
+ }
+
+ pub fn textBufferFromString(ctx: Context, str: []const u8) !TextBuffer {
+ const allocator = ctx.allocator;
+ var al = try ArrayListUnmanaged(u8).initCapacity(allocator, @max(16, str.len));
+ try al.appendSlice(allocator, str);
+ return .{
+ .allocator = allocator,
+ .al = al,
+ };
+ }
+};
+
+pub const TextBuffer = struct {
+ allocator: Allocator,
+ al: ArrayListUnmanaged(u8) = .{},
+ cursor_index: GlyphIndex = 0,
+ focus: bool = false,
+
+ pub fn deinit(text_buffer: *TextBuffer) void {
+ text_buffer.al.deinit(text_buffer.allocator);
+ }
+
+ pub fn toString(text_buffer: TextBuffer) []u8 {
+ return text_buffer.al.items;
+ }
+
+ pub fn len(tb: TextBuffer) u32 {
+ return tb.al.items.len;
+ }
+
+ pub fn cursorMoveLeft(tb: *TextBuffer) void {
+ while (tb.cursor_index > 0) {
+ tb.cursor_index -= 1;
+ if ((tb.al.items[@intCast(tb.cursor_index)] & 0b1100_0000) != 0b1000_0000) return;
+ }
+ }
+
+ pub fn cursorMoveRight(tb: *TextBuffer) void {
+ while (tb.cursor_index < tb.al.items.len) {
+ tb.cursor_index += 1;
+ if (tb.cursor_index == tb.al.items.len) return;
+ if ((tb.al.items[@intCast(tb.cursor_index)] & 0b1100_0000) != 0b1000_0000) return;
+ }
+ }
+
+ pub fn getCodepointIndexAtCursor(tb: TextBuffer) u32 {
+ var iter = std.unicode.Utf8Iterator{.bytes = tb.toString(), .i = 0};
+ var i: i32 = 0;
+ var codepoint_index: u32 = 0;
+ while (true) {
+ if (i == tb.cursor_index) return codepoint_index;
+ codepoint_index += 1;
+ const cp = iter.nextCodepoint() orelse return codepoint_index;
+ const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable;
+ i += cp_len;
+ }
+ }
+};
+
+// p in [0..1]
+pub fn setMousePos(p: V2) void {
+ mouse_pos = p;
+}
+
+// === RENDERING ===
+
+pub fn clear(buffer: []u8) void {
+ @memset(buffer, 0);
+}
+
+pub const RenderQueue = struct {
+ var allocator: Allocator = undefined;
+ var threads: []Thread = undefined;
+ pub var cpu_count: usize = undefined;
+ var work_queue: ArrayListUnmanaged(CallArgs) = .{};
+ var work_mutex = Thread.Mutex{};
+ var work_enqueued = Thread.Condition{};
+ var work_done = Thread.Condition{};
+ var run: std.atomic.Value(bool) = .init(true);
+ var currently_rendering_threads: std.atomic.Value(u32) = .init(0);
+
+ pub fn init(allocator_: Allocator) !void {
+ allocator = allocator_;
+ cpu_count = @max(1, try Thread.getCpuCount());
+ //cpu_count = 1;
+ threads = try allocator.alloc(Thread, cpu_count);
+ work_queue = try .initCapacity(allocator, cpu_count);
+ for (threads) |*t| {
+ t.* = try Thread.spawn(.{.allocator = allocator}, doWork, .{});
+ }
+ }
+
+ pub fn deinit() void {
+ run.store(false, .seq_cst);
+ work_enqueued.broadcast();
+ for (threads) |t| {
+ t.join();
+ }
+
+ allocator.free(threads);
+ work_queue.deinit(allocator);
+ }
+
+ pub const CallArgs = struct {
+ target: []u8,
+ img_pixels: []const V4, img_offset: V2, img_size: V2, img_stride: f32, img_factor: V2,
+ color: Color,
+ pos: V2i, size: V2i,
+ pixels_per_row: i32,
+ y_start: i32, group_y_size: u32
+ };
+
+ // NOTE: order in which work will be executed is undefined
+ pub fn dispatch(work: CallArgs) !void {
+ {
+ work_mutex.lock();
+ defer work_mutex.unlock();
+ try work_queue.append(allocator, work);
+ }
+ work_enqueued.signal();
+ }
+
+ pub fn waitUntilAllFinished() void {
+ work_mutex.lock();
+ defer work_mutex.unlock();
+
+ while (work_queue.items.len > 0 or currently_rendering_threads.load(.seq_cst) > 0) {
+ work_done.wait(&work_mutex);
+ }
+ }
+
+ fn doWork() void {
+ while (run.load(.seq_cst)) {
+ var work: ?CallArgs = null;
+ {
+ work_mutex.lock();
+ defer work_mutex.unlock();
+
+ if (work_queue.items.len == 0) {
+ work_enqueued.wait(&work_mutex);
+ }
+
+ if (work_queue.items.len > 0) {
+ work = work_queue.pop();
+ }
+ }
+ if (work) |w| {
+ _ = currently_rendering_threads.rmw(.Add, 1, .seq_cst);
+ @call(.auto, drawRectThreaded, .{w});
+ _ = currently_rendering_threads.rmw(.Sub, 1, .seq_cst);
+ work_mutex.lock();
+ defer work_mutex.unlock();
+ work_done.broadcast();
+ }
+ }
+ }
+};
+
+pub fn drawRect(target: []u8, image: ?Image, img_rect: Rect, color: Color, pos: @Vector(2, f32), size: @Vector(2, f32), upixels_per_row: u32, rendering_text: bool) !void {
+ const pixels_per_row = @as(i32, @intCast(upixels_per_row));
+ const single_white_pixel = [1]@Vector(4, f32){.{1,1,1,1}};
+ var img_pixels: []const V4 = &single_white_pixel;
+ var img_offset: V2 = .{0,0};
+ var img_size: V2 = .{1,1};
+ var img_factor: V2 = .{0, 0};
+ var img_stride: f32 = 1;
+ if (image) |img| {
+ img_pixels = @ptrCast(img.pixels);
+ img_offset = img.size * img_rect.pos;
+ img_size = img.size * img_rect.size;
+ img_factor = img_size / size;
+ img_stride = img.size[0];
+ }
+ const ipos = @as(V2i, @intFromFloat(pos));
+ const isiz = @as(V2i, @intFromFloat(size));
+
+ if (isiz[0] == 0 or isiz[1] == 0) return;
+ const render_thread_count: u32 = blk: {
+ const minimum_pixels_per_thread = 128; // somewhat arbitrary, but this gives good results on my Ryzen 5 3600
+ const target_cpu_count = (size[0] * size[1] / minimum_pixels_per_thread);
+ break :blk @max(1, @min(@as(u32, @intFromFloat(target_cpu_count)), RenderQueue.cpu_count));
+ };
+ const group_y_count = @abs(isiz[1])/render_thread_count;
+ if (!rendering_text) {
+ // if we're rendering text, we know there won't be any overdraw,
+ // so we don't have to wait for the previous rects to finish rendering.
+ // We handle beginning and end synchronization in `draw`
+ RenderQueue.waitUntilAllFinished();
+ }
+ for (0..render_thread_count) |i| {
+ const y_start: i32 = @intCast(group_y_count*i);
+ const call_args = RenderQueue.CallArgs{
+ .target = target,
+ .img_pixels = img_pixels, .img_offset = img_offset, .img_size = img_size, .img_stride = img_stride, .img_factor = img_factor,
+ .color = color,
+ .pos = ipos, .size = isiz,
+ .pixels_per_row = pixels_per_row,
+ .y_start = y_start, .group_y_size = if (i == render_thread_count - 1) @as(u32, @intCast(isiz[1] - y_start)) else group_y_count
+ };
+ try RenderQueue.dispatch(call_args);
+ }
+}
+
+pub fn drawRectThreaded(args: RenderQueue.CallArgs) void {
+ const target = args.target;
+ const img_pixels = args.img_pixels;
+ const img_offset = args.img_offset;
+ const img_size = args.img_size;
+ _ = img_size;
+ const img_stride = args.img_stride;
+ const img_factor = args.img_factor;
+ const color = args.color;
+ const pos = args.pos;
+ const size = args.size;
+ const pixels_per_row = args.pixels_per_row;
+ const y_start = args.y_start;
+ const group_y_size = args.group_y_size;
+
+ var y: i32 = y_start;
+ for (0..group_y_size) |_| {
+ defer y += 1;
+ const yy = (pos[1] + y) * pixels_per_row;
+ const fy = @as(f32, @floatFromInt(y));
+ const img_y = @floor(img_factor[1] * fy + img_offset[1]);
+ var x: i32 = 0;
+ for (0..@intCast(size[0])) |_| {
+ defer x += 1;
+ const idx = @as(u32, @intCast((pos[0] + x + yy) * 4));
+ const img_x = img_factor[0] * @as(f32, @floatFromInt(x)) + img_offset[0];
+ const fimage_idx = @floor(img_y * img_stride + img_x);
+ const image_idx = @as(usize, @intFromFloat(fimage_idx));
+ const image_value = img_pixels[image_idx];
+ if (image_value[3] == 0) continue; // skip transparent (~10% performance increase when rendering text, barely measurable decrease for solid rectangle)
+ const final_value = color.vec * image_value;
+
+ const prev_pixel = V4{
+ @as(f32, @floatFromInt(target[idx+0])),
+ @as(f32, @floatFromInt(target[idx+1])),
+ @as(f32, @floatFromInt(target[idx+2])),
+ @as(f32, @floatFromInt(target[idx+3]))
+ } / @as(V4, @splat(255));
+ const alpha = final_value[3];
+ const alphad_value = final_value * V4{alpha,alpha,alpha,1} + prev_pixel * @as(V4, @splat(1-alpha));
+
+ const final_vvalue: @Vector(4, u8) = @intFromFloat(alphad_value * @as(@Vector(4, f32), @splat(255)));
+ const final_ivalue: [4]u8 = final_vvalue; // @sizeOf(@Vector(4, u8)) is undefined
+ const final_ptr = @as([]const u8, @ptrCast(&final_ivalue));
+ assert(final_ptr.len == 4);
+ @memcpy(target[idx..idx+4], final_ptr);
+ }
+ }
+}
+
+// === C ABI ===
+
+pub fn exportCAbi () void {
+ if (!@inComptime()) @compileError("Must be called at comptime");
+ const prefix = "";
+ const c_compat = struct {
+ const root = @import("root");
+ pub const CRect = extern struct {
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
+ };
+
+ pub const InitFn = fn () callconv(.c) bool;
+ pub const LoopFn = fn (dt: f32) callconv(.c) bool;
+ pub const DeinitFn = fn () callconv(.c) void;
+ pub fn c_run(
+ init_fn: *const InitFn,
+ loop_fn: *const LoopFn,
+ deinit_fn: *const DeinitFn,
+ ) callconv(.c) bool {
+ const static = struct {
+ pub const Pixel = packed struct {
+ r: u8 = 0,
+ g: u8 = 0,
+ b: u8 = 0,
+ _a: u8 = undefined,
+ };
+ var pixel_buffer: *const []Pixel = undefined;
+ var window_size: @Vector(2, u32) = undefined;
+ var fwindow_size: @Vector(2, f32) = undefined;
+ pub fn setWindowSize(ws: @Vector(2, c_uint)) void {
+ window_size = @intCast(ws);
+ fwindow_size = @floatFromInt(window_size);
+ pixel_buffer = @ptrCast(root.pixels);
+ }
+
+ pub var internal_init: *const InitFn = undefined;
+ pub fn init_wrapper() !void {
+ try globalInit(root.allocator);
+ errdefer globalDeinit();
+ if (!internal_init()) return error.InternalError;
+ }
+ pub var internal_loop: *const LoopFn = undefined;
+ pub fn loop_wrapper(dt: f32) !bool {
+ if (!internal_loop(dt)) return false;
+ clear(@ptrCast(pixel_buffer.*));
+ var context_iter = contexts.iterator(0);
+ while (context_iter.next()) |ctx| {
+ try ctx.draw(.{
+ .pixels = @ptrCast(pixel_buffer.*),
+ .bytes_per_pixel = @sizeOf(Pixel),
+ .pixels_per_row = window_size[0],
+ });
+ }
+ return true;
+ }
+
+ pub var internal_deinit: *const DeinitFn = undefined;
+ pub fn deinit_wrapper() void {
+ internal_deinit();
+ globalDeinit();
+ contexts.clearAndFree(root.allocator);
+ }
+ };
+ static.internal_init = init_fn;
+ static.internal_loop = loop_fn;
+ static.internal_deinit = deinit_fn;
+ root.run(
+ static.init_wrapper,
+ static.setWindowSize,
+ static.loop_wrapper,
+ static.deinit_wrapper
+ ) catch return false;
+ return true;
+ }
+
+ var contexts = std.SegmentedList(Context, 4){};
+ pub fn c_context_init() callconv(.c) ?*Context {
+ var ctx = Context.init(.{.allocator = root.allocator}) catch return null;
+ contexts.append(root.allocator, ctx) catch {
+ ctx.deinit();
+ return null;
+ };
+ return contexts.at(contexts.count()-1);
+ }
+ pub fn c_context_deinit(ctx: *Context) callconv(.c) void {
+ ctx.deinit();
+ var i: usize = 0;
+ var context_iter = contexts.iterator(0);
+ while (context_iter.next()) |registered_context| {
+ defer i += 1;
+ if (registered_context == ctx) {
+ contexts.at(i).* = contexts.at(contexts.count()-1).*;
+ _ = contexts.pop();
+ break;
+ }
+ }
+ }
+
+ const CRectangleOptions = extern struct {
+ color: [4]f32,
+ image: ?*Image,
+ hover: ?*bool,
+ consume_hover: bool,
+ ignore_hover_if_consumed: bool,
+ begin_drag_if_hover_and: bool,
+ end_drag_if: bool,
+ drag: ?*V2,
+ };
+ pub fn c_rectangle(ctx: *Context, rect: CRect, o: CRectangleOptions) callconv(.c) bool {
+ ctx.rectangle(.{.pos = .{rect.x, rect.y}, .size = .{rect.w, rect.h}}, .{
+ .color = .{.vec = o.color},
+ .image = if (o.image) |img| img.* else null,
+ .hover = o.hover,
+ .consume_hover = o.consume_hover,
+ .ignore_hover_if_consumed = o.ignore_hover_if_consumed,
+ .begin_drag_if_hover_and = o.begin_drag_if_hover_and,
+ .end_drag_if = o.end_drag_if,
+ //.drag = TODO
+ }) catch return false;
+ return true;
+ }
+ };
+ @export(&c_compat.c_run, .{.name = prefix++"run", .linkage = .strong});
+ @export(&c_compat.c_context_init, .{.name = prefix++"context_init", .linkage = .strong});
+ @export(&c_compat.c_context_deinit, .{.name = prefix++"context_deinit", .linkage = .strong});
+ @export(&c_compat.c_rectangle, .{.name = prefix++"rectangle_", .linkage = .strong});
+}
+
diff --git a/src/linux.zig b/src/linux.zig
new file mode 100644
index 0000000..d2c38a3
--- /dev/null
+++ b/src/linux.zig
@@ -0,0 +1,319 @@
+const std = @import("std");
+const mem = std.mem;
+
+const x = @import("x11.zig");
+const input = @import("app/input.zig");
+const gui = @import("app/ui.zig");
+const app = @import("app/app.zig");
+
+const Allocator = mem.Allocator;
+const ArrayList = std.ArrayList;
+
+const assert = std.debug.assert;
+
+pub var allocator: Allocator = undefined;
+pub var target_fps: ?f32 = null;
+
+var empty_pixels: []u32 = &[0]u32{};
+pub var pixels: *[]u32 = &empty_pixels;
+
+pub var utf8_of_keys_pressed: []u8 = &.{};
+
+pub fn main() !void {
+ try run(app.init, app.setWindowSize, app.loop, app.deinit);
+}
+
+pub fn run(
+ comptime init_fn: anytype,
+ comptime set_window_size: anytype,
+ comptime loop_fn: anytype,
+ comptime deinit_fn: anytype,
+) !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{.stack_trace_frames = 16}){};
+ allocator = gpa.allocator();
+ defer {
+ _ = gpa.deinit();
+ }
+
+ var display = try x.Display.open();
+ defer display.close();
+
+ const screen = x.Screen.default(display);
+
+ const root_window = x.c.RootWindow(@as(*x.c.Display, @ptrCast(display)), screen.id);
+
+ var swa = mem.zeroes(x.c.XSetWindowAttributes);
+ swa.event_mask = x.c.ExposureMask | x.c.KeyPressMask | x.c.KeyReleaseMask | x.c.StructureNotifyMask;
+ var window_size = @Vector(2, c_uint){ 720, 720 };
+ var window = try x.Window.create(.{
+ .display = display,
+ .parent = .{ .window = .{ .id = root_window } },
+ .pos = .{ 500, 500 },
+ .size = window_size,
+ .depth = display.defaultDepth(screen),
+ .visual = display.defaultVisual(screen),
+ .value_mask = x.c.CWColormap | x.c.CWEventMask,
+ .attributes = &swa,
+ });
+ defer window.destroy(display);
+
+ display.mapWindow(window);
+
+ _ = x.c.XStoreName(@ptrCast(display), window.id, "minimgui window");
+
+ var screen_buffers: ScreenBuffers = undefined;
+ createScreenBuffers(&screen_buffers, display, screen, window_size);
+ defer destroyScreenBuffers(&screen_buffers, display);
+ var cur_img_idx: u8 = 0;
+ var cur_img = screen_buffers.imgs[cur_img_idx];
+
+ {
+ var raw_pixel_data: [*]u32 = @alignCast(@ptrCast(cur_img.data));
+ pixels.* = @ptrCast(raw_pixel_data[0..window_size[0]*window_size[1]]);
+ }
+ set_window_size(window_size);
+
+ //_ = std.c.setlocale(.CTYPE, "");
+ //_ = x.c.XSetLocaleModifiers("");
+ //if (x.c.XSupportsLocale() == 0) return error.XDoesntSupportLocale;
+
+ display.selectInput(
+ window,
+ x.c.ExposureMask |
+ x.c.KeyPressMask | x.c.KeyReleaseMask |
+ x.c.ButtonPressMask | x.c.ButtonReleaseMask |
+ x.c.PointerMotionMask |
+ x.c.StructureNotifyMask
+ );
+
+ const wm_delete_msg = display.internAtom("WM_DELETE_WINDOW", false);
+ var protocols = [_]x.Atom{wm_delete_msg};
+ display.setWmProtocols(window, &protocols);
+
+ const xim = x.c.XOpenIM(@ptrCast(display), null, null, null);
+ const xic = x.c.XCreateIC(xim,
+ x.c.XNInputStyle, x.c.XIMPreeditNothing | x.c.XIMStatusNothing,
+ x.c.XNClientWindow, window.id,
+ x.c. XNFocusWindow, window.id,
+ @as(?*anyopaque, null)
+ );
+
+ try input._init(.{});
+ defer input._deinit();
+
+ try init_fn();
+ defer deinit_fn();
+
+ var delta_time: f32 = 1.0 / 60.0; // TODO this is a lie
+ var reset = true;
+ app: while (true) {
+ const frame_start_time_us = std.time.microTimestamp();
+ defer delta_time = @as(f32, @floatFromInt(std.time.microTimestamp() - frame_start_time_us))/1e6;
+
+ var buf_utf8_of_keys_pressed: [32]u8 = undefined;
+ utf8_of_keys_pressed = &.{};
+ if (reset) input._before_poll();
+ while (display.pending() > 0) {
+ var event = display.nextEvent();
+ switch (event.type) {
+ x.c.Expose => {},
+ x.c.FocusIn => {
+ x.c.XSetICFocus(xic);
+ std.debug.print("focus\n", .{});
+ },
+ x.c.FocusOut => {
+ x.c.XUnsetICFocus(xic);
+ std.debug.print("unfocus\n", .{});
+ },
+ x.c.ConfigureNotify => {
+ const new_window_size: @Vector(2, c_uint) = @intCast(@Vector(2, c_int){event.xconfigure.width, event.xconfigure.height});
+ if (@reduce(.And, new_window_size == window_size)) continue;
+
+ destroyScreenBuffers(&screen_buffers, display);
+ _ = x.c.XFlush(@ptrCast(display));
+
+ window_size = @intCast(@Vector(2, c_int){event.xconfigure.width, event.xconfigure.height});
+ createScreenBuffers(&screen_buffers, display, screen, window_size);
+ _ = x.c.XFlush(@ptrCast(display));
+
+ cur_img = screen_buffers.imgs[cur_img_idx];
+ const raw_pixel_data: [*]u32 = @alignCast(@ptrCast(cur_img.data));
+ pixels.* = @ptrCast(raw_pixel_data[0..window_size[0]*window_size[1]]);
+
+ set_window_size(window_size);
+ },
+ x.c.ClientMessage => if (event.xclient.data.l[0] == wm_delete_msg) break :app,
+ x.c.MotionNotify => {
+ const raw_pointer_pos = @Vector(2, f32){@as(f32, @floatFromInt(event.xmotion.x)), @as(f32, @floatFromInt(event.xmotion.y))};
+ const fwindow_size: @Vector(2, f32) = @floatFromInt(window_size);
+ const norm_pointer_pos = raw_pointer_pos / fwindow_size;
+ const scale_factor = @max(fwindow_size[0], fwindow_size[1]) / @min(fwindow_size[0], fwindow_size[1]);
+ const wide_window = fwindow_size[0] > fwindow_size[1];
+ const ui_pointer_pos = norm_pointer_pos * if (wide_window) @Vector(2, f32){scale_factor, 1} else @Vector(2, f32){1, scale_factor};
+ input.simulate(.{.pointer_axis = .{.axis = .x}}, ui_pointer_pos[0]);
+ input.simulate(.{.pointer_axis = .{.axis = .y}}, ui_pointer_pos[1]);
+ gui.setMousePos(ui_pointer_pos);
+ },
+ x.c.KeyPress => {
+ var evt = event.xkey;
+ const keysym = x.c.XLookupKeysym(&evt, 0);
+ const device_input = input.DeviceInput{.key = .{.code = keysym}};
+ input.simulate(device_input, 1);
+
+ if (display.pending() > 0) {
+ const next_event = display.peekEvent();
+ if (
+ next_event.type == x.c.KeyRelease and
+ next_event.xkey.time == event.xkey.time and
+ next_event.xkey.keycode == event.xkey.keycode
+ ) {
+ _ = display.nextEvent();
+ }
+ }
+
+ if (x.c.XFilterEvent(&event, x.c.None) != 0) continue;
+ var status: x.c.Status = undefined;
+ const len = x.c.Xutf8LookupString(xic, &evt, &buf_utf8_of_keys_pressed, buf_utf8_of_keys_pressed.len, null, &status); // TODO get keysym here
+ switch (status) {
+ x.c.XBufferOverflow => return error.XBufferOverflow,
+ x.c.XLookupNone => {},
+ x.c.XLookupKeySym => return error.TODO,
+ x.c.XLookupChars,
+ x.c.XLookupBoth => {}, // OK
+ else => unreachable,
+ }
+ utf8_of_keys_pressed = buf_utf8_of_keys_pressed[0..@intCast(len)];
+ },
+ x.c.KeyRelease => {
+ var skip_release = false;
+ if (display.pending() > 0) {
+ const next_event = display.peekEvent();
+ if (
+ next_event.type == x.c.KeyPress and
+ next_event.xkey.time == event.xkey.time or
+ next_event.xkey.keycode == event.xkey.keycode
+ ) {
+ skip_release = true;
+ }
+ }
+ if (!skip_release) {
+ var evt = event.xkey;
+ const keysym = x.c.XLookupKeysym(&evt, 0);
+ input.simulate(.{.key = .{.code = keysym}}, 0);
+ }
+ },
+ x.c.ButtonPress => {
+ if (event.xbutton.button == 4) {
+ input.simulate(.{.pointer_axis = .{.axis = .scroll_y}}, 1);
+ } else if (event.xbutton.button == 5) {
+ input.simulate(.{.pointer_axis = .{.axis = .scroll_y}}, -1);
+ } else {
+ input.simulate(.{.pointer_button = .{.button = event.xbutton.button}}, 1);
+ }
+ },
+ x.c.ButtonRelease => {
+ input.simulate(.{.pointer_button = .{.button = event.xbutton.button}}, 0);
+ },
+ else => std.debug.print("Unexpected event type: {d}\n", .{event.type}),
+ }
+ }
+
+ reset = try loop_fn(delta_time);
+
+ if (reset) {
+ _ = x.c.XShmPutImage(
+ @ptrCast(display),
+ window.id,
+ display.defaultGC(screen),
+ cur_img, 0, 0, 0, 0, window_size[0], window_size[1], x.c.False,
+ );
+ cur_img_idx = (cur_img_idx + 1) % @as(u8, @intCast(screen_buffers.imgs.len));
+ cur_img = screen_buffers.imgs[cur_img_idx];
+ const raw_pixel_data: [*]u32 = @alignCast(@ptrCast(cur_img.data));
+ pixels.* = @ptrCast(raw_pixel_data[0..window_size[0]*window_size[1]]);
+
+ _ = x.c.XFlush(@ptrCast(display));
+ }
+
+ // WAIT FOR TARGET FPS
+ const tfps = target_fps orelse 800; // FIXME: at high FPS (>~800) elements sometimes aren't drawn and appear to "flicker"
+ const now = std.time.microTimestamp();
+ const wait_until = frame_start_time_us + @as(u32, @intFromFloat(1/tfps*std.time.us_per_s));
+ if (now < wait_until) {
+ const wait_delay = wait_until - now;
+ std.Thread.sleep(@intCast(wait_delay * std.time.ns_per_us));
+ }
+ }
+}
+
+pub const ScreenBuffers = struct {
+ imgs: [2]*x.c.XImage,
+ shminfos: [2]x.c.XShmSegmentInfo,
+};
+
+pub fn createScreenBuffers(screen_buffers: *ScreenBuffers, display: *x.Display, screen: x.Screen, window_size: [2]c_uint) void {
+ for (&screen_buffers.imgs, &screen_buffers.shminfos) |*img, *shminfo| {
+ img.* = x.c.XShmCreateImage(
+ @ptrCast(display),
+ display.defaultVisual(screen),
+ @intCast(display.defaultDepth(screen)),
+ x.c.ZPixmap, null,
+ shminfo,
+ window_size[0], window_size[1],
+ );
+ shminfo.shmid = x.c.shmget(x.c.IPC_PRIVATE, @intCast(img.*.bytes_per_line * img.*.height), x.c.IPC_CREAT | 0o777);
+ img.*.data = @ptrCast(x.c.shmat(shminfo.shmid, null, 0));
+ shminfo.shmaddr = img.*.data;
+ shminfo.readOnly = x.c.False;
+ _ = x.c.XShmAttach(@ptrCast(display), shminfo);
+ }
+ _ = x.c.XSync(@ptrCast(display), x.c.False);
+}
+
+inline fn XDestroyImage(ximage: anytype) c_int {
+ const func: *const fn (*x.c.XImage) callconv(.c) c_int = @ptrCast(ximage.*.f.destroy_image);
+ return func(ximage);
+}
+pub fn destroyScreenBuffers(sb: *ScreenBuffers, display: *x.Display) void {
+ for (&sb.imgs, &sb.shminfos) |*img, *shminfo| {
+ _ = x.c.XShmDetach(@ptrCast(display), shminfo); //_ = x.c.XDestroyImage(img); // Can't do this because zig doesn't translate it correctly
+ _ = XDestroyImage(img.*);
+
+
+ _ = x.c.shmdt(shminfo.shmaddr);
+ _ = x.c.shmctl(shminfo.shmid, x.c.IPC_RMID, 0);
+ }
+}
+
+pub const Key = x.c.KeySym;
+pub fn getPlatformKey(key: input.CommonKey) Key {
+ return switch (key) {
+ .w => x.c.XK_w,
+ .a => x.c.XK_a,
+ .s => x.c.XK_s,
+ .d => x.c.XK_d,
+ .left_shift => x.c.XK_Shift_L,
+ .right_shift => x.c.XK_Shift_R,
+ .left_control => x.c.XK_Control_L,
+ .right_control => x.c.XK_Control_R,
+ .arrow_left => x.c.XK_Left,
+ .arrow_right => x.c.XK_Right,
+ else => {
+ @panic("TODO define all");
+ },
+ };
+}
+
+pub const Button = c_uint;
+pub fn getPlatformButton(key: input.CommonButton) Button {
+ return switch (key) {
+ .left => x.c.Button1,
+ .middle => x.c.Button2,
+ .right => x.c.Button3,
+ };
+}
+
+comptime { if (@import("builtin").output_mode == .Lib) {
+ gui.exportCAbi();
+} }
+
diff --git a/src/x11.zig b/src/x11.zig
new file mode 100644
index 0000000..e348b35
--- /dev/null
+++ b/src/x11.zig
@@ -0,0 +1,331 @@
+const std = @import("std");
+
+pub const c = @cImport({
+ @cInclude("X11/Xlib.h");
+ @cInclude("X11/Xutil.h");
+ @cInclude("X11/extensions/XShm.h");
+ @cInclude("sys/shm.h");
+ @cInclude("GL/glx.h");
+});
+
+pub const Atom = c.Atom;
+
+fn from(x: bool) c.Bool {
+ return if (x) c.True else c.False;
+}
+
+pub const GraphicsContext = struct {
+ const Shape = enum(c_int) {
+ complex = c.Complex,
+ convex = c.Convex,
+ non_convex = c.Nonconvex,
+ };
+ const Mode = enum(c_int) {
+ coord_origin = c.CoordModeOrigin,
+ coord_previous = c.CoordModePrevious,
+ };
+
+ display: *Display,
+ window: Window,
+ gc: c.GC,
+
+ pub fn drawLine(ctx: GraphicsContext, p1: @Vector(2, c_int), p2: @Vector(2, c_int)) void {
+ _ = c.XDrawLine(@ptrCast(ctx.display), ctx.window.id, ctx.gc, p1[0], p1[1], p2[0], p2[1]);
+ }
+
+ pub fn fillRectangle(ctx: GraphicsContext, pos: @Vector(2, c_int), size: @Vector(2, c_uint)) void {
+ _ = c.XFillRectangle(@ptrCast(ctx.display), ctx.window.id, ctx.gc, pos[0], pos[1], size[0], size[1]);
+ }
+
+ pub fn fillPolygon(ctx: GraphicsContext, points: []@Vector(2, c_short), shape: Shape, mode: Mode) void {
+ _ = c.XFillPolygon(
+ @ptrCast(ctx.display),
+ ctx.window.id,
+ ctx.gc,
+ @ptrCast(points.ptr),
+ @intCast(points.len),
+ @intFromEnum(shape),
+ @intFromEnum(mode),
+ );
+ }
+
+ pub fn drawString(ctx: GraphicsContext, pos: @Vector(2, c_int), string: []const u8) void {
+ _ = c.XDrawString(
+ @ptrCast(ctx.display),
+ ctx.window.id,
+ ctx.gc,
+ pos[0],
+ pos[1],
+ string.ptr,
+ @intCast(string.len),
+ );
+ }
+
+ pub fn setBackground(ctx: GraphicsContext, color: c_ulong) void {
+ _ = c.XSetBackground(@ptrCast(ctx.display), ctx.gc, color);
+ }
+
+ pub fn setForeground(ctx: GraphicsContext, color: c_ulong) void {
+ _ = c.XSetForeground(@ptrCast(ctx.display), ctx.gc, color);
+ }
+
+ pub fn free(ctx: GraphicsContext) void {
+ _ = c.XFreeGC(@ptrCast(ctx.display), ctx.gc);
+ }
+};
+
+pub const Display = opaque {
+ pub fn open() !*Display {
+ var display: *c.Display = undefined;
+ display = c.XOpenDisplay(null) orelse return error.FailedToOpenWindow;
+ return @ptrCast(display);
+ }
+
+ pub fn close(display: *Display) void {
+ _ = c.XCloseDisplay(@ptrCast(display));
+ }
+
+ pub fn selectInput(display: *Display, window: Window, event_mask: c_long) void {
+ _ = c.XSelectInput(@ptrCast(display), window.id, event_mask);
+ }
+
+ pub fn mapWindow(display: *Display, window: Window) void {
+ _ = c.XMapWindow(@ptrCast(display), window.id);
+ }
+
+ pub fn createGc(display: *Display, window: Window, value_mask: c_ulong, values: [*c]c.XGCValues) GraphicsContext {
+ return .{
+ .display = display,
+ .window = window,
+ .gc = c.XCreateGC(@ptrCast(display), window.id, value_mask, values),
+ };
+ }
+
+ pub fn internAtom(display: *Display, atom_name: [:0]const u8, only_if_existds: bool) Atom {
+ return c.XInternAtom(@ptrCast(display), atom_name.ptr, from(only_if_existds));
+ }
+
+ pub fn setWmProtocols(display: *Display, window: Window, protocols: []Atom) void {
+ _ = c.XSetWMProtocols(@ptrCast(display), window.id, protocols.ptr, @intCast(protocols.len));
+ }
+
+ pub fn pending(display: *Display) c_int {
+ return c.XPending(@ptrCast(display));
+ }
+
+ pub fn nextEvent(display: *Display) c.XEvent {
+ var xevent: c.XEvent = undefined;
+ _ = c.XNextEvent(@ptrCast(display), &xevent);
+ return xevent;
+ }
+
+ pub fn peekEvent(display: *Display) c.XEvent {
+ var xevent: c.XEvent = undefined;
+ _ = c.XPeekEvent(@ptrCast(display), &xevent);
+ return xevent;
+ }
+
+ pub fn displayKeycodes(display: *Display, min_keycode: *c_int, max_keycode: *c_int) void {
+ _ = c.XDisplayKeycodes(@ptrCast(display), min_keycode, max_keycode);
+ }
+
+ pub fn grabPointer(
+ display: *Display,
+ window: Window,
+ owner_events: bool,
+ event_mask: c_uint,
+ pointer_mode: c_int,
+ keyboard_mode: c_int,
+ confine_to: Window,
+ cursor: c.Cursor,
+ time: c.Time,
+ ) void {
+ _ = c.XGrabPointer(
+ @ptrCast(display),
+ window.id,
+ from(owner_events),
+ event_mask,
+ pointer_mode,
+ keyboard_mode,
+ confine_to.id,
+ cursor,
+ time,
+ );
+ }
+
+ pub fn defaultDepth(display: *Display, screen: Screen) c_int {
+ return c.DefaultDepth(@as(*c.Display, @ptrCast(display)), screen.id);
+ }
+
+ pub fn defaultVisual(display: *Display, screen: Screen) *c.Visual {
+ return c.DefaultVisual(@as(*c.Display, @ptrCast(display)), screen.id) orelse @panic("unreachable?");
+ }
+
+ pub fn defaultGC(display: *Display, screen: Screen) c.GC {
+ return c.DefaultGC(@as(*c.Display, @ptrCast(display)), screen.id) orelse @panic("unreachable?");
+ }
+
+ pub fn blackPixel(display: *Display, screen: Screen) c_ulong {
+ return c.BlackPixel(display, screen.id);
+ }
+
+ pub fn whitePixel(display: *Display, screen: Screen) c_ulong {
+ return c.WhitePixel(display, screen.id);
+ }
+
+ // --- GLX ---
+ pub fn glChooseVisual(display: *Display, screen: Screen, attrib_list: [:0]const c_int) *c.XVisualInfo {
+ return c.glXChooseVisual(@ptrCast(display), screen.id, @constCast(attrib_list.ptr)) orelse @panic("TODO");
+ }
+
+ pub fn glCreateContext(
+ display: *Display,
+ // vi: *c.XVisualInfo,
+ fb_config: c.GLXFBConfig,
+ share_list: c.GLXContext,
+ direct: bool,
+ context_attribs: []c_int,
+ ) c.GLXContext {
+ const GlXCreateContextAttribsARBProc = *const fn (
+ *c.Display,
+ c.GLXFBConfig,
+ share_list: c.GLXContext,
+ c.Bool,
+ [*c]c_int,
+ ) callconv(.C) c.GLXContext;
+ const glXCreateContextAttribsARBProc = @as(
+ GlXCreateContextAttribsARBProc,
+ @ptrCast(c.glXGetProcAddressARB(@ptrCast("glXCreateContextAttribsARB".ptr))),
+ );
+ return glXCreateContextAttribsARBProc(@ptrCast(display), fb_config, share_list, from(direct), context_attribs.ptr);
+ // return c.glXCreateContext(@ptrCast(display), vi, share_list, from(direct));
+ }
+
+ pub fn glDestroyContext(display: *Display, glc: c.GLXContext) void {
+ c.glXDestroyContext(@ptrCast(display), glc);
+ }
+
+ pub fn glMakeCurrent(display: *Display, window: Window, glc: c.GLXContext) void {
+ _ = c.glXMakeCurrent(@ptrCast(display), window.id, glc);
+ }
+
+ pub fn glSwapBuffers(display: *Display, window: Window) void {
+ c.glXSwapBuffers(@ptrCast(display), window.id);
+ }
+
+ pub fn glSwapInterval(display: *Display, drawable: anytype, interval: c_int) void {
+ const T = @TypeOf(drawable);
+ std.debug.assert(T == Window); // TODO
+ internal.glSwapInterval(display, drawable.id, interval);
+ }
+};
+
+pub const Screen = struct {
+ id: c_int,
+
+ pub fn default(display: *Display) Screen {
+ return .{ .id = c.DefaultScreen(@as(*c.Display, @ptrCast(display))) };
+ }
+};
+
+pub const Window = struct {
+ id: c.Window,
+
+ pub fn root(display: *Display, screen: Screen) Window {
+ return .{ .id = c.XRootWindow(@ptrCast(display), screen.id) };
+ }
+
+ pub fn createSimple(args: struct {
+ display: *Display,
+ parent: union(enum) { window: Window, root: Screen },
+ pos: @Vector(2, c_int),
+ size: @Vector(2, c_uint),
+ border_width: c_uint,
+ border: c_ulong,
+ background: c_ulong,
+ }) !Window {
+ const parent = switch (args.parent) {
+ .window => |wnd| wnd,
+ .root => |screen| root(args.display, screen),
+ };
+ const window = .{ .id = c.XCreateSimpleWindow(
+ @ptrCast(args.display),
+ parent.id,
+ args.pos[0],
+ args.pos[1],
+ args.size[0],
+ args.size[1],
+ args.border_width,
+ args.border,
+ args.background,
+ ) };
+ if (window.id == c.None) return error.FailedToCreateWindow;
+ return window;
+ }
+
+ pub fn create(args: struct {
+ display: *Display,
+ parent: union(enum) { window: Window, root: Screen },
+ pos: @Vector(2, c_int),
+ size: @Vector(2, c_uint),
+ border_width: c_uint = 0,
+ depth: c_int,
+ class: c_uint = c.InputOutput,
+ visual: *c.Visual,
+ value_mask: c_ulong,
+ attributes: *c.XSetWindowAttributes,
+ }) !Window {
+ const parent = switch (args.parent) {
+ .window => |wnd| wnd,
+ .root => |screen| root(args.display, screen),
+ };
+ const window = Window{ .id = c.XCreateWindow(
+ @ptrCast(args.display),
+ parent.id,
+ args.pos[0],
+ args.pos[1],
+ args.size[0],
+ args.size[1],
+ args.border_width,
+ args.depth,
+ args.class,
+ args.visual,
+ args.value_mask,
+ args.attributes,
+ ) };
+ if (window.id == c.None) return error.FailedToCreateWindow;
+ return window;
+ }
+
+ pub fn destroy(window: Window, display: *Display) void {
+ _ = c.XDestroyWindow(@ptrCast(display), window.id);
+ }
+};
+
+pub fn initGl() !void {
+ // TODO glXGetProcAddress returns non-null even when procname doesn't exist. Might want to handle that.
+ const GlInternal = @import("root").gl.internal;
+ inline for (comptime std.meta.declarations(GlInternal)) |field| {
+ comptime var proc_name: [field.name.len + 2:0]u8 = undefined;
+ comptime {
+ proc_name[0] = 'g';
+ proc_name[1] = 'l';
+ for (field.name, proc_name[2..]) |field_c, *proc_c| {
+ proc_c.* = field_c;
+ }
+ proc_name[2] = std.ascii.toUpper(proc_name[2]);
+ }
+ const workaround = proc_name;
+ @field(GlInternal, field.name) = @ptrCast(c.glXGetProcAddress(&workaround) orelse return error.GlGetProcAddressFailed);
+ }
+
+ //TODO(steven) check if extension available first
+ internal.glSwapInterval = @ptrCast(c.glXGetProcAddress("glXSwapIntervalEXT") orelse return error.GlGetProcAddressFailed);
+
+ @import("root").gl.is_initialized = true;
+}
+
+pub const internal = struct {
+ pub var glSwapInterval: *const fn (display: *Display, drawable: c.GLXDrawable, interval: c_int) callconv(.C) void = undefined;
+};
+