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(); } }