diff --git a/examples/01_client_connect.zig b/examples/01_client_connect.zig index 2f9b33f..84aabf5 100644 --- a/examples/01_client_connect.zig +++ b/examples/01_client_connect.zig @@ -4,8 +4,32 @@ const xkbcommon = @import("xkbcommon"); const font8x8 = @cImport({ @cInclude("font8x8.h"); }); +const PieceTable = @import("PieceTable.zig").PieceTable; const Pixel = [4]u8; +const Theme = struct { + const background = 0x282A36; + const current_line = 0x44475A; + const foreground = 0xF8F8F2; + const comment = 0x6272A4; + + const cyan = 0x8BE9FD; + const green = 0x50FA7B; + const orange = 0xFFB86C; + const pink = 0xFF79C6; + const purple = 0xBD93F9; + const red = 0xFF5555; + const yellow = 0xF1FA8C; +}; + +fn toPixel(color: u24) Pixel { + return .{ + @intCast(color & 0xFF), + @intCast(color >> 8 & 0xFF), + @intCast(color >> 16 & 0xFF), + 0xFF, + }; +} pub fn main() !void { var general_allocator = std.heap.GeneralPurposeAllocator(.{}){}; @@ -27,6 +51,7 @@ pub fn main() !void { wayland.xdg.WmBase, wayland.core.Seat, wayland.zxdg.DecorationManagerV1, + wayland.zwp.TextInputManagerV3, }); const DISPLAY_ID = 1; @@ -34,6 +59,7 @@ pub fn main() !void { const compositor_id = ids[1] orelse return error.NeccessaryWaylandExtensionMissing; const xdg_wm_base_id = ids[2] orelse return error.NeccessaryWaylandExtensionMissing; const wl_seat_id = ids[3] orelse return error.NeccessaryWaylandExtensionMissing; + const zwp_text_input_manager_v3 = ids[5] orelse return error.NeccessaryWaylandExtensionMissing; const surface_id = id_pool.create(); try conn.send( @@ -63,6 +89,16 @@ pub fn main() !void { } }, ); + const zwp_text_input_v3_id = id_pool.create(); + try conn.send( + wayland.zwp.TextInputManagerV3.Request, + zwp_text_input_manager_v3, + .{ .get_text_input = .{ + .id = zwp_text_input_v3_id, + .seat = wl_seat_id, + } }, + ); + var zxdg_toplevel_decoration_id_opt: ?u32 = null; if (ids[4]) |zxdg_decoration_manager_id| { zxdg_toplevel_decoration_id_opt = id_pool.create(); @@ -265,6 +301,20 @@ pub fn main() !void { xkb_state.unref(); }; + var piece_table = try PieceTable.init(gpa, "Hello, World!"); + defer piece_table.deinit(); + + var edit_buffer: [1024]u8 = [1]u8{0} ** 1024; + var edit_slice: ?[]u8 = null; + + var delete_before: usize = 0; + var delete_after: usize = 0; + + // var preedit_buffer: [1024]u8 = [1]u8{0} ** 1024; + // var preedit_slice: ?[]u8 = null; + + var cursor_pos: usize = piece_table.getTotalSize(); + var running = true; while (running) { const header, const body = try conn.recv(); @@ -288,8 +338,10 @@ pub fn main() !void { // put some interesting colors into the new_framebuffer renderGradient(new_framebuffer, window_size); + const text = try piece_table.writeAllAlloc(); + defer gpa.free(text); // blit some characters - renderText(new_framebuffer, window_size, .{ 10, 10 }, "Hello, World!"); + renderText(new_framebuffer, window_size, .{ 10, 10 }, text); try conn.send( wayland.core.ShmPool.Request, @@ -325,12 +377,6 @@ pub fn main() !void { } }, ); - try conn.send( - wayland.core.Surface.Request, - surface_id, - wayland.core.Surface.Request.commit, - ); - // commit the configuration try conn.send( wayland.core.Surface.Request, @@ -390,25 +436,180 @@ pub fn main() !void { xkb_keymap_opt = xkbcommon.Keymap.newFromString(xkb_ctx, @ptrCast(mem), .text_v1, .no_flags) orelse return error.XKBKeymap; xkb_state_opt = xkbcommon.State.new(xkb_keymap_opt.?) orelse return error.XKBStateInit; }, + .modifiers => |mods| { + if (xkb_state_opt) |xkb_state| { + _ = xkb_state.updateMask( + mods.mods_depressed, + mods.mods_latched, + mods.mods_locked, + 0, + 0, + 0, + ); + } + }, .key => |key| { if (xkb_state_opt) |xkb_state| { const keycode: xkbcommon.Keycode = key.key + 8; const keysym: xkbcommon.Keysym = xkb_state.keyGetOneSym(keycode); var buf: [64]u8 = undefined; - const name_len = keysym.getName(&buf, buf.len); - std.debug.print("{s}\n", .{buf[0..@intCast(name_len)]}); + // const name_len = keysym.getName(&buf, buf.len); + // std.debug.print("{s}\n", .{buf[0..@intCast(name_len)]}); - const changed = if (key.state == .pressed) - xkb_state.updateKey(keycode, .down) - else - xkb_state.updateKey(keycode, .up); - _ = changed; + if (key.state == .pressed) { + const sym = xkbcommon.Keysym; + switch (@as(u32, @intFromEnum(keysym))) { + sym.BackSpace => { + try piece_table.delete(cursor_pos - 1, 1); + cursor_pos -= 1; + }, + sym.Delete => { + piece_table.delete(cursor_pos, 1) catch |e| switch (e) { + error.OutOfBounds => {}, + else => return e, + }; + }, + sym.Left => { + cursor_pos -|= 1; + }, + sym.Right => { + cursor_pos += 1; + cursor_pos = @min(cursor_pos, piece_table.getTotalSize()); + }, + else => if (key.state == .pressed) { + const size = xkb_state.keyGetUtf8(keycode, &buf); + try piece_table.insert(cursor_pos, buf[0..size]); + cursor_pos += size; + }, + } + + const new_buffer_id, const new_framebuffer = try getFramebuffer(&framebuffers, &id_pool, pool_alloc, window_size); + + // put some interesting colors into the new_framebuffer + renderGradient(new_framebuffer, window_size); + + const text = try piece_table.writeAllAlloc(); + defer gpa.free(text); + // blit some characters + renderText(new_framebuffer, window_size, .{ 10, 10 }, text); + + try conn.send( + wayland.core.ShmPool.Request, + wl_shm_pool_id, + .{ .create_buffer = .{ + .new_id = new_buffer_id, + .offset = @intCast(@intFromPtr(new_framebuffer.ptr) - @intFromPtr(pool_bytes.ptr)), + .width = @intCast(window_size[0]), + .height = @intCast(window_size[1]), + .stride = @as(i32, @intCast(window_size[0])) * @sizeOf([4]u8), + .format = .argb8888, + } }, + ); + + try conn.send( + wayland.core.Surface.Request, + surface_id, + .{ .attach = .{ + .buffer = new_buffer_id, + .x = 0, + .y = 0, + } }, + ); + + try conn.send( + wayland.core.Surface.Request, + surface_id, + .{ .damage = .{ + .x = 0, + .y = 0, + .width = std.math.maxInt(i32), + .height = std.math.maxInt(i32), + } }, + ); + + // commit the configuration + try conn.send( + wayland.core.Surface.Request, + surface_id, + wayland.core.Surface.Request.commit, + ); + } } }, else => { std.debug.print("<- wl_keyboard@{}\n", .{event}); }, } + } else if (header.object_id == zwp_text_input_v3_id) { + const event = try wayland.deserialize(wayland.zwp.TextInputV3.Event, header, body); + std.debug.print("<- zwp_text_input_v3@{} event {}\n", .{ zwp_text_input_v3_id, event }); + switch (event) { + .enter => |e| { + _ = e; + + // if (e.surface == surface_id) { + try conn.send( + wayland.zwp.TextInputV3.Request, + zwp_text_input_v3_id, + .enable, + ); + + try conn.send( + wayland.zwp.TextInputV3.Request, + zwp_text_input_v3_id, + .{ .set_content_type = .{ + .hint = .multiline, + .purpose = .normal, + } }, + ); + + try conn.send( + wayland.zwp.TextInputV3.Request, + zwp_text_input_v3_id, + .commit, + ); + // } + }, + .leave => |e| { + _ = e; + + // if (e.surface == surface_id) { + try conn.send( + wayland.zwp.TextInputV3.Request, + zwp_text_input_v3_id, + .disable, + ); + // } + }, + .preedit_string => {}, + .commit_string => |commit| { + edit_slice = edit_buffer[0..commit.text.len]; + @memcpy(edit_slice.?, commit.text); + }, + .delete_surrounding_text => |offset| { + delete_before = offset.before_length; + delete_after = offset.after_length; + }, + .done => |_| { + // 1 replace existing pre-edit string with cursor + // 2 delete requested surrounding text + const start = cursor_pos - delete_before; + const end = cursor_pos + delete_after; + const length = end - start; + if (length != 0) { + try piece_table.delete(start, length); + } + // 3 insert commit string with cursor at its end + if (edit_slice) |slice| { + try piece_table.insert(cursor_pos, slice); + cursor_pos += slice.len; + edit_slice = null; + } + // 4 calculate surrounding text to send + // 5 insert new preedit text in cursor position + // 6 place cursor inside predit text + }, + } } else if (framebuffers.get(header.object_id)) |framebuffer_slice| { const event = try wayland.deserialize(wayland.core.Buffer.Event, header, body); switch (event) { @@ -440,6 +641,13 @@ fn cmsg(comptime T: type) type { }; } +fn getFramebuffer(framebuffers: *std.AutoHashMap(u32, []Pixel), id_pool: *wayland.IdPool, pool_alloc: std.mem.Allocator, fb_size: [2]u32) !struct { u32, []Pixel } { + const new_buffer_id = id_pool.create(); + const new_framebuffer = try pool_alloc.alloc(Pixel, fb_size[0] * fb_size[1]); + try framebuffers.put(new_buffer_id, new_framebuffer); + return .{ new_buffer_id, new_framebuffer }; +} + fn renderGradient(framebuffer: []Pixel, fb_size: [2]u32) void { for (0..fb_size[1]) |y| { const row = framebuffer[y * fb_size[0] .. (y + 1) * fb_size[0]]; @@ -472,19 +680,9 @@ fn renderText(framebuffer: []Pixel, fb_size: [2]u32, pos: [2]usize, str: []const const char = font8x8.font8x8_basic[which_char]; const line = char[(y - top) % 8]; if ((line >> @intCast((x - left) % 8)) & 0x1 != 0) { - pixel.* = .{ - 0xFF, - 0xFF, - 0xFF, - 0xFF, - }; + pixel.* = toPixel(Theme.foreground); } else { - pixel.* = .{ - 0x00, - 0x00, - 0x00, - 0xFF, - }; + pixel.* = toPixel(Theme.background); } } } diff --git a/examples/PieceTable.zig b/examples/PieceTable.zig new file mode 100644 index 0000000..90a184a --- /dev/null +++ b/examples/PieceTable.zig @@ -0,0 +1,449 @@ +const std = @import("std"); +const testing = std.testing; + +/// Represents buffer and a set of changes to the buffer. +/// +/// All insertions are copied internally. Deletions to the buffer will not free memory. +/// +/// To initialize without text, use struct initialization syntax and specify an allocator +/// like so: `var table = PieceTable{ .allocator = allocator };` +pub const PieceTable = struct { + allocator: std.mem.Allocator, + buffers: std.ArrayListUnmanaged([]u8) = .{}, + pieces: std.ArrayListUnmanaged(Piece) = .{}, + + pub const Piece = struct { + slice: []const u8, + tag: Tag = .added, + pub const Tag = enum { + original, + added, + }; + }; + + pub fn init(allocator: std.mem.Allocator, original: []const u8) !PieceTable { + if (original.len == 0) { + // An empty string was passed, skip making a copy of it + return .{ + .allocator = allocator, + }; + } + + const original_copy = try allocator.dupe(u8, original); + + // Store original text + var buffers = try std.ArrayListUnmanaged([]u8).initCapacity(allocator, 1); + buffers.appendAssumeCapacity(original_copy); + + // Create piece pointing to original text + var pieces = try std.ArrayListUnmanaged(Piece).initCapacity(allocator, 1); + pieces.appendAssumeCapacity(.{ + .slice = original_copy, + .tag = .original, + }); + + return .{ + .allocator = allocator, + .buffers = buffers, + .pieces = pieces, + }; + } + + pub fn deinit(table: *PieceTable) void { + for (table.buffers.items) |buffer| { + table.allocator.free(buffer); + } + table.buffers.deinit(table.allocator); + table.pieces.deinit(table.allocator); + } + + /// Inserts `new_text` into buffer at `index`. + /// + /// `new_text` is owned by caller. + /// + /// It is an error to insert outside of the bounds of the piece table. If the + /// table is empty, 0 is the only valid argument for index. + pub fn insert(table: *PieceTable, index: usize, new_text: []const u8) !void { + const text = try table.allocator.dupe(u8, new_text); + try table.buffers.append(table.allocator, text); + + // Insert at the start of the file, catches empty tables + if (index == 0) { + try table.pieces.insert(table.allocator, 0, .{ .slice = text, .tag = .added }); + return; + } + + var p_i: usize = 0; + var b_i: usize = 0; + while (p_i < table.pieces.items.len) : (p_i += 1) { + const p = table.pieces.items[p_i]; + if (index == b_i + p.slice.len) { + if (p_i + 1 == table.pieces.items.len) { + // The new index is the end of the file + try table.pieces.append(table.allocator, .{ .slice = text, .tag = .added }); + return; + } else { + // The new index is directly after an existing node, but not at the end of the file. + try table.pieces.insert(table.allocator, p_i + 1, .{ .slice = text, .tag = .added }); + return; + } + } else if (index < b_i + p.slice.len) { + // new piece is within another piece; split the old one into 2 + // and insert the new piece between + + // ignore the returned slice since we will also want the + // piece right before the insertion + _ = try table.pieces.addManyAt(table.allocator, p_i + 1, 2); + const pieces = table.pieces.items[p_i..][0..3]; + + const sub_i = index - b_i; + + pieces[0].slice = p.slice[0..sub_i]; + pieces[1].slice = text; + pieces[2].slice = p.slice[sub_i..]; + + // set the tag for the pieces 1 and 2 + // elide setting the tag for pieces[0], it should be correct already + pieces[1].tag = .added; + switch (p.tag) { + .original => pieces[2].tag = .original, + .added => pieces[2].tag = .added, + } + return; + } else { + b_i += p.slice.len; + } + } + + @panic("Impossible state while inserting into PieceTable"); + } + + /// Deletes the data from start to start+length from the piece table. + /// Will not free any memory. + pub fn delete(table: *PieceTable, start: usize, length: usize) !void { + if (length == 0) return error.InvalidLength; + const endi = start + length; + + var b_i: usize = 0; // buffer index + const p_start, const start_subi, const start_piece = for (table.pieces.items, 0..) |piece, i| { + if (start < b_i + piece.slice.len) { + // start found + break .{ i, start - b_i, piece }; + } + b_i += piece.slice.len; + } else return error.OutOfBounds; + + // reuse b_i + const p_end, const end_subi, const end_piece = for (table.pieces.items[p_start..], p_start..) |piece, i| { + if (endi < b_i + piece.slice.len) { + break .{ i, endi - b_i, piece }; + } + b_i += piece.slice.len; + } else .{ p_start, start_piece.slice.len, start_piece }; + + // Removal cases: + // 1. the deletion starts on one piece boundary and ends on another piece boundary + // - Delete all pieces between start and end + // 2. the deletion starts within a piece and ends on a boundary + // - Delete all but the start piece + // - modify slice end in start piece + // 3. the deletion starts on a bondary and ends within a piece + // - Delete all but the end piece + // - modify slice start in end piece + // 4. the deletion starts within a piece and ends within a piece + // - Delet all the start and end pieces + // - modify slice end in start piece + // - modify slice end in end piece + + const is_start_on_boundary = start_subi == 0; + const is_end_on_boundary = end_subi == end_piece.slice.len; + + const remove_len = (p_end + 1) - p_start; + if (is_start_on_boundary and is_end_on_boundary) { + table.pieces.replaceRange(table.allocator, p_start, remove_len, &.{}) catch unreachable; + } else { + if (is_start_on_boundary) { + const new = &[_]PieceTable.Piece{ + .{ .slice = end_piece.slice[end_subi..], .tag = end_piece.tag }, + }; + table.pieces.replaceRange(table.allocator, p_start, remove_len, new) catch unreachable; + } else if (is_end_on_boundary) { + const new = &[_]PieceTable.Piece{ + .{ .slice = start_piece.slice[0..start_subi], .tag = start_piece.tag }, + }; + table.pieces.replaceRange(table.allocator, p_start, remove_len, new) catch unreachable; + } else { + const new = &[_]PieceTable.Piece{ + .{ .slice = start_piece.slice[0..start_subi], .tag = start_piece.tag }, + .{ .slice = end_piece.slice[end_subi..], .tag = end_piece.tag }, + }; + table.pieces.replaceRange(table.allocator, p_start, remove_len, new) catch unreachable; + } + } + } + + pub fn getTotalSize(table: PieceTable) usize { + var length: usize = 0; + for (table.pieces.items) |piece| { + length += piece.slice.len; + } + return length; + } + + pub fn writeAll(table: PieceTable, buffer: []u8) void { + std.debug.assert(table.getTotalSize() == buffer.len); + var current_buffer = buffer[0..]; + for (table.pieces.items) |piece| { + @memcpy(current_buffer[0..piece.slice.len], piece.slice); + current_buffer = current_buffer[piece.slice.len..]; + } + } + + pub fn writeAllAlloc(table: PieceTable) ![]u8 { + const size = table.getTotalSize(); + const buffer = try table.allocator.alloc(u8, size); + var current_buffer = buffer[0..]; + for (table.pieces.items) |piece| { + @memcpy(current_buffer[0..piece.slice.len], piece.slice); + current_buffer = current_buffer[piece.slice.len..]; + } + return buffer; + } +}; + +test "Init empty PieceTable" { + var table = try PieceTable.init(testing.allocator, ""); + defer table.deinit(); + + var out_buf: [0]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings("", &out_buf); +} + +test "Insert into empty PieceTable" { + var table = try PieceTable.init(testing.allocator, ""); + defer table.deinit(); + + try table.insert(0, "the quick brown fox\njumped over the lazy dog"); + + var out_buf: [44]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\jumped over the lazy dog + , &out_buf); +} + +test "Init Piecetable" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + var out_buf: [44]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\jumped over the lazy dog + , &out_buf); +} + +test "Insert into PieceTable" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(20, "went to the park and\n"); + + try testing.expectEqual(@as(usize, 3), table.pieces.items.len); + try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[0].tag); + try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[1].tag); + try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[2].tag); + + try testing.expectEqualStrings("the quick brown fox\n", table.pieces.items[0].slice); + try testing.expectEqualStrings("went to the park and\n", table.pieces.items[1].slice); + try testing.expectEqualStrings("jumped over the lazy dog", table.pieces.items[2].slice); + + try testing.expectEqual(@as(usize, 65), table.getTotalSize()); + + var out_buf: [65]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\went to the park and + \\jumped over the lazy dog + , &out_buf); +} + +test "Insert at end of Piece" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(20, "went to the park and\n"); + try table.insert(41, "ate a burger and\n"); + + try testing.expectEqual(@as(usize, 4), table.pieces.items.len); + try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[0].tag); + try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[1].tag); + try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[2].tag); + try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[3].tag); + + try testing.expectEqualStrings("the quick brown fox\n", table.pieces.items[0].slice); + try testing.expectEqualStrings("went to the park and\n", table.pieces.items[1].slice); + try testing.expectEqualStrings("ate a burger and\n", table.pieces.items[2].slice); + try testing.expectEqualStrings("jumped over the lazy dog", table.pieces.items[3].slice); + + try testing.expectEqual(@as(usize, 82), table.getTotalSize()); + + var out_buf: [82]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\went to the park and + \\ate a burger and + \\jumped over the lazy dog + , &out_buf); +} + +test "Insert at end of file" { + const original = "the quick brown fox"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(19, "\njumped over the lazy dog"); + + try testing.expectEqual(@as(usize, 2), table.pieces.items.len); + try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[0].tag); + try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[1].tag); + + try testing.expectEqualStrings("the quick brown fox", table.pieces.items[0].slice); + try testing.expectEqualStrings("\njumped over the lazy dog", table.pieces.items[1].slice); + + try testing.expectEqual(@as(usize, 44), table.getTotalSize()); + + var out_buf: [44]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\jumped over the lazy dog + , &out_buf); +} + +test "Delete one entire Piece" { + const original = "the quick brown fox"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.delete(0, 19); + try testing.expectEqual(@as(usize, 0), table.pieces.items.len); +} + +test "Delete multiple entire Pieces" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(20, "went to the park and\n"); + try table.insert(41, "ate a burger and\n"); + try table.delete(20, 38); + + try testing.expectEqual(@as(usize, 2), table.pieces.items.len); +} + +test "Delete inside a Piece" { + const original = "the quick brown fox"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + // delete "brown " + try table.delete(10, 6); + + try testing.expectEqual(@as(usize, 2), table.pieces.items.len); + try testing.expectEqualStrings("the quick ", table.pieces.items[0].slice); + try testing.expectEqualStrings("fox", table.pieces.items[1].slice); + + try testing.expectEqual(@as(usize, 13), table.getTotalSize()); + + var out_buf: [13]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick fox + , &out_buf); +} + +test "Delete from within one piece to within another piece" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(20, "went to the park and\n"); + try table.insert(41, "ate a burger and\n"); + try table.delete(45, 13 + 12); + + try testing.expectEqual(@as(usize, 4), table.pieces.items.len); + + try testing.expectEqual(@as(usize, 57), table.getTotalSize()); + + var out_buf: [57]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\went to the park and + \\ate the lazy dog + , &out_buf); +} + +test "Delete from start of piece to within piece" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(20, "went to the park and\n"); + try table.insert(41, "ate a burger and\n"); + try table.delete(41, 13); + + try testing.expectEqual(@as(usize, 4), table.pieces.items.len); + + try testing.expectEqual(@as(usize, 69), table.getTotalSize()); + + var out_buf: [69]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\went to the park and + \\and + \\jumped over the lazy dog + , &out_buf); +} + +test "Delete from within piece to end of piece" { + const original = "the quick brown fox\njumped over the lazy dog"; + var table = try PieceTable.init(testing.allocator, original); + defer table.deinit(); + + try table.insert(20, "went to the park and\n"); + try table.insert(41, "ate a burger and\n"); + try table.delete(45, 13); + + try testing.expectEqual(@as(usize, 4), table.pieces.items.len); + + try testing.expectEqual(@as(usize, 69), table.getTotalSize()); + + var out_buf: [69]u8 = undefined; + table.writeAll(&out_buf); + + try testing.expectEqualStrings( + \\the quick brown fox + \\went to the park and + \\ate jumped over the lazy dog + , &out_buf); +} diff --git a/src/main.zig b/src/main.zig index 75e869d..90126bc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,7 @@ const testing = std.testing; pub const core = @import("./core.zig"); pub const xdg = @import("./xdg.zig"); pub const zxdg = @import("./zxdg.zig"); +pub const zwp = @import("./zwp.zig"); pub const types = @import("./types.zig"); pub fn getDisplayPath(gpa: std.mem.Allocator) ![]u8 { @@ -82,10 +83,11 @@ pub fn readInt(buffer: []const u32, parent_pos: *usize) !i32 { return int; } -pub fn readString(buffer: []const u32, parent_pos: *usize) ![:0]const u8 { +pub fn readString(buffer: []const u32, parent_pos: *usize) !?[:0]const u8 { var pos = parent_pos.*; const len = try readUInt(buffer, &pos); + if (len == 0) return null; const wordlen = std.mem.alignForward(usize, len, @sizeOf(u32)) / @sizeOf(u32); if (pos + wordlen > buffer.len) return error.EndOfStream; @@ -126,12 +128,21 @@ pub fn deserializeArguments(comptime Signature: type, buffer: []const u32) !Sign }, .Pointer => |ptr| switch (ptr.size) { .Slice => if (ptr.child == u8) { - @field(result, field.name) = try readString(buffer, &pos); + @field(result, field.name) = try readString(buffer, &pos) orelse return error.UnexpectedNullString; } else { @field(result, field.name) = try readArray(ptr.child, buffer, &pos); }, else => @compileError("Unsupported type " ++ @typeName(field.type)), }, + .Optional => |opt| switch (@typeInfo(opt.child)) { + .Pointer => |ptr| switch (ptr.size) { + .Slice => if (ptr.child == u8) { + @field(result, field.name) = try readString(buffer, &pos); + } else @compileError("Unsupported type " ++ @typeName(field.type)), + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, else => @compileError("Unsupported type " ++ @typeName(field.type)), } } @@ -232,6 +243,27 @@ pub fn serializeArguments(comptime Signature: type, buffer: []u32, message: Sign }, else => @compileError("Unsupported type " ++ @typeName(field.type)), }, + .Optional => |opt| switch (@typeInfo(opt.child)) { + .Pointer => |ptr| switch (ptr.size) { + .Slice => if (ptr.child == u8) { + const str = @field(message, field.name); + if (str.len >= std.math.maxInt(u32)) return error.StringTooLong; + + buffer[pos] = @intCast(str.len + 1); + pos += 1; + + const str_len_aligned = std.mem.alignForward(usize, str.len + 1, @sizeOf(u32)); + const padding_len = str_len_aligned - str.len; + if (str_len_aligned / @sizeOf(u32) >= buffer[pos..].len) return error.OutOfMemory; + const buffer_bytes = std.mem.sliceAsBytes(buffer[pos..]); + @memcpy(buffer_bytes[0..str.len], str); + @memset(buffer_bytes[str.len..][0..padding_len], 0); + pos += str_len_aligned / @sizeOf(u32); + } else @compileError("Unsupported type " ++ @typeName(field.type)), + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, + else => @compileError("Unsupported type " ++ @typeName(field.type)), + }, else => @compileError("Unsupported type " ++ @typeName(field.type)), } } @@ -496,6 +528,7 @@ pub const Conn = struct { conn.send_buffer = try conn.allocator.realloc(conn.send_buffer, conn.send_buffer.len * 2); continue; }, + else => return e, }; break msg; diff --git a/src/zwp.zig b/src/zwp.zig new file mode 100644 index 0000000..96c506d --- /dev/null +++ b/src/zwp.zig @@ -0,0 +1,117 @@ +pub const TextInputManagerV3 = struct { + pub const INTERFACE = "zwp_text_input_manager_v3"; + pub const VERSION = 1; + + pub const Request = union(Request.Tag) { + destroy: void, + get_text_input: struct { + id: u32, + seat: u32, + }, + pub const Tag = enum(u16) { + destroy, + get_text_input, + }; + }; +}; + +pub const TextInputV3 = struct { + pub const Request = union(Tag) { + destroy, + enable, + disable, + set_surrounding_text: struct { + text: []const u8, + cursor: i32, + anchor: i32, + }, + set_text_change_cause: struct { + cause: ChangeCause, + }, + set_content_type: struct { + hint: ContentHint, + purpose: ContentPurpose, + }, + set_cursor_rectangle: struct { + x: i32, + y: i32, + width: i32, + height: i32, + }, + commit, + pub const Tag = enum(u16) { + destroy, + enable, + disable, + set_surrounding_text, + set_text_change_cause, + set_content_type, + set_cursor_rectangle, + commit, + }; + }; + + pub const Event = union(Tag) { + enter: struct { surface: u32 }, + leave: struct { surface: u32 }, + preedit_string: struct { + text: ?[]const u8, + cursor_begin: i32, + cursor_end: i32, + }, + commit_string: struct { + text: []const u8, + }, + delete_surrounding_text: struct { + before_length: usize, + after_length: usize, + }, + done: struct { + serial: u32, + }, + pub const Tag = enum { + enter, + leave, + preedit_string, + commit_string, + delete_surrounding_text, + done, + }; + }; + + pub const ChangeCause = enum(u32) { + input_method, + other, + }; + + pub const ContentHint = enum(u32) { + none, + completion, + spellcheck, + auto_capitalization, + lowercase, + uppercase, + titlecase, + hidden_text, + sensitive_data, + latin, + multiline, + }; + + pub const ContentPurpose = enum(u32) { + normal, + alpha, + digits, + number, + phone, + url, + email, + name, + password, + pin, + date, + time, + datetime, + terminal, + }; +};