feat: add text editing

dev
Louis Pearson 2024-01-18 00:51:43 -07:00
parent dad4b23cf5
commit e87f718f5d
4 changed files with 825 additions and 28 deletions

View File

@ -4,8 +4,32 @@ const xkbcommon = @import("xkbcommon");
const font8x8 = @cImport({ const font8x8 = @cImport({
@cInclude("font8x8.h"); @cInclude("font8x8.h");
}); });
const PieceTable = @import("PieceTable.zig").PieceTable;
const Pixel = [4]u8; 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 { pub fn main() !void {
var general_allocator = std.heap.GeneralPurposeAllocator(.{}){}; var general_allocator = std.heap.GeneralPurposeAllocator(.{}){};
@ -27,6 +51,7 @@ pub fn main() !void {
wayland.xdg.WmBase, wayland.xdg.WmBase,
wayland.core.Seat, wayland.core.Seat,
wayland.zxdg.DecorationManagerV1, wayland.zxdg.DecorationManagerV1,
wayland.zwp.TextInputManagerV3,
}); });
const DISPLAY_ID = 1; const DISPLAY_ID = 1;
@ -34,6 +59,7 @@ pub fn main() !void {
const compositor_id = ids[1] orelse return error.NeccessaryWaylandExtensionMissing; const compositor_id = ids[1] orelse return error.NeccessaryWaylandExtensionMissing;
const xdg_wm_base_id = ids[2] 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 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(); const surface_id = id_pool.create();
try conn.send( 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; var zxdg_toplevel_decoration_id_opt: ?u32 = null;
if (ids[4]) |zxdg_decoration_manager_id| { if (ids[4]) |zxdg_decoration_manager_id| {
zxdg_toplevel_decoration_id_opt = id_pool.create(); zxdg_toplevel_decoration_id_opt = id_pool.create();
@ -265,6 +301,20 @@ pub fn main() !void {
xkb_state.unref(); 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; var running = true;
while (running) { while (running) {
const header, const body = try conn.recv(); const header, const body = try conn.recv();
@ -288,8 +338,10 @@ pub fn main() !void {
// put some interesting colors into the new_framebuffer // put some interesting colors into the new_framebuffer
renderGradient(new_framebuffer, window_size); renderGradient(new_framebuffer, window_size);
const text = try piece_table.writeAllAlloc();
defer gpa.free(text);
// blit some characters // blit some characters
renderText(new_framebuffer, window_size, .{ 10, 10 }, "Hello, World!"); renderText(new_framebuffer, window_size, .{ 10, 10 }, text);
try conn.send( try conn.send(
wayland.core.ShmPool.Request, 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 // commit the configuration
try conn.send( try conn.send(
wayland.core.Surface.Request, 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_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; 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| { .key => |key| {
if (xkb_state_opt) |xkb_state| { if (xkb_state_opt) |xkb_state| {
const keycode: xkbcommon.Keycode = key.key + 8; const keycode: xkbcommon.Keycode = key.key + 8;
const keysym: xkbcommon.Keysym = xkb_state.keyGetOneSym(keycode); const keysym: xkbcommon.Keysym = xkb_state.keyGetOneSym(keycode);
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
const name_len = keysym.getName(&buf, buf.len); // const name_len = keysym.getName(&buf, buf.len);
std.debug.print("{s}\n", .{buf[0..@intCast(name_len)]}); // std.debug.print("{s}\n", .{buf[0..@intCast(name_len)]});
const changed = if (key.state == .pressed) if (key.state == .pressed) {
xkb_state.updateKey(keycode, .down) const sym = xkbcommon.Keysym;
else switch (@as(u32, @intFromEnum(keysym))) {
xkb_state.updateKey(keycode, .up); sym.BackSpace => {
_ = changed; 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 => { else => {
std.debug.print("<- wl_keyboard@{}\n", .{event}); 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| { } else if (framebuffers.get(header.object_id)) |framebuffer_slice| {
const event = try wayland.deserialize(wayland.core.Buffer.Event, header, body); const event = try wayland.deserialize(wayland.core.Buffer.Event, header, body);
switch (event) { 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 { fn renderGradient(framebuffer: []Pixel, fb_size: [2]u32) void {
for (0..fb_size[1]) |y| { for (0..fb_size[1]) |y| {
const row = framebuffer[y * fb_size[0] .. (y + 1) * fb_size[0]]; 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 char = font8x8.font8x8_basic[which_char];
const line = char[(y - top) % 8]; const line = char[(y - top) % 8];
if ((line >> @intCast((x - left) % 8)) & 0x1 != 0) { if ((line >> @intCast((x - left) % 8)) & 0x1 != 0) {
pixel.* = .{ pixel.* = toPixel(Theme.foreground);
0xFF,
0xFF,
0xFF,
0xFF,
};
} else { } else {
pixel.* = .{ pixel.* = toPixel(Theme.background);
0x00,
0x00,
0x00,
0xFF,
};
} }
} }
} }

449
examples/PieceTable.zig Normal file
View File

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

View File

@ -3,6 +3,7 @@ const testing = std.testing;
pub const core = @import("./core.zig"); pub const core = @import("./core.zig");
pub const xdg = @import("./xdg.zig"); pub const xdg = @import("./xdg.zig");
pub const zxdg = @import("./zxdg.zig"); pub const zxdg = @import("./zxdg.zig");
pub const zwp = @import("./zwp.zig");
pub const types = @import("./types.zig"); pub const types = @import("./types.zig");
pub fn getDisplayPath(gpa: std.mem.Allocator) ![]u8 { pub fn getDisplayPath(gpa: std.mem.Allocator) ![]u8 {
@ -82,10 +83,11 @@ pub fn readInt(buffer: []const u32, parent_pos: *usize) !i32 {
return int; 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.*; var pos = parent_pos.*;
const len = try readUInt(buffer, &pos); const len = try readUInt(buffer, &pos);
if (len == 0) return null;
const wordlen = std.mem.alignForward(usize, len, @sizeOf(u32)) / @sizeOf(u32); const wordlen = std.mem.alignForward(usize, len, @sizeOf(u32)) / @sizeOf(u32);
if (pos + wordlen > buffer.len) return error.EndOfStream; 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) { .Pointer => |ptr| switch (ptr.size) {
.Slice => if (ptr.child == u8) { .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 { } else {
@field(result, field.name) = try readArray(ptr.child, buffer, &pos); @field(result, field.name) = try readArray(ptr.child, buffer, &pos);
}, },
else => @compileError("Unsupported type " ++ @typeName(field.type)), 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)), 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)), 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)), 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); conn.send_buffer = try conn.allocator.realloc(conn.send_buffer, conn.send_buffer.len * 2);
continue; continue;
}, },
else => return e,
}; };
break msg; break msg;

117
src/zwp.zig Normal file
View File

@ -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,
};
};