diff options
| author | Steven Van Dorp <steven@vandorp.lu> | 2026-02-06 10:00:08 +0100 |
|---|---|---|
| committer | Steven Van Dorp <steven@vandorp.lu> | 2026-02-06 10:00:08 +0100 |
| commit | 0d8aeb6464103ec8e35c10c448bf08b0b9edc156 (patch) | |
| tree | 80ab47bf3e0f137856d8494753e774c8456c85c8 /src/linux.zig | |
Diffstat (limited to 'src/linux.zig')
| -rw-r--r-- | src/linux.zig | 319 |
1 files changed, 319 insertions, 0 deletions
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(); +} } + |
