diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/app.zig | 126 | ||||
| -rw-r--r-- | src/app/input.zig | 281 | ||||
| -rw-r--r-- | src/app/ui.zig | 1054 | ||||
| -rw-r--r-- | src/linux.zig | 319 | ||||
| -rw-r--r-- | src/x11.zig | 331 |
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; +}; + |
