-8
build.zig
-8
build.zig
···
3
3
pub fn build(b: *std.Build) void {
4
4
const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true;
5
5
const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true;
6
-
const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true;
7
6
const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false;
8
7
9
8
const options = b.addOptions();
10
9
options.addOption(bool, "libxev", include_libxev);
11
10
options.addOption(bool, "images", include_images);
12
-
options.addOption(bool, "text_input", include_text_input);
13
11
options.addOption(bool, "aio", include_aio);
14
12
15
13
const options_mod = options.createModule();
···
27
25
.optimize = optimize,
28
26
.target = target,
29
27
}) else null;
30
-
const gap_buffer_dep = if (include_text_input) b.lazyDependency("gap_buffer", .{
31
-
.optimize = optimize,
32
-
.target = target,
33
-
}) else null;
34
28
const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{
35
29
.optimize = optimize,
36
30
.target = target,
···
50
44
vaxis_mod.addImport("grapheme", zg_dep.module("grapheme"));
51
45
vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
52
46
if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg"));
53
-
if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer"));
54
47
if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev"));
55
48
if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio"));
56
49
if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro"));
···
100
93
tests.root_module.addImport("grapheme", zg_dep.module("grapheme"));
101
94
tests.root_module.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
102
95
if (zigimg_dep) |dep| tests.root_module.addImport("zigimg", dep.module("zigimg"));
103
-
if (gap_buffer_dep) |dep| tests.root_module.addImport("gap_buffer", dep.module("gap_buffer"));
104
96
tests.root_module.addImport("build_options", options_mod);
105
97
106
98
const tests_run = b.addRunArtifact(tests);
-4
build.zig.zon
-4
build.zig.zon
···
8
8
.hash = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5",
9
9
.lazy = true,
10
10
},
11
-
.gap_buffer = .{
12
-
.url = "git+https://github.com/ryleelyman/GapBuffer.zig#9039708e09fc3eb5f698ab5694a436afe503c6a6",
13
-
.hash = "1220f525973ae804ec0284556bfc47db7b6a8dc86464a853956ef859d6e0fb5fa93b",
14
-
},
15
11
.zg = .{
16
12
.url = "git+https://codeberg.org/dude_the_builder/zg?ref=master#689ab6b83d08c02724b99d199d650ff731250998",
17
13
.hash = "12200d1ce5f9733a9437415d85665ad5fbc85a4d27689fd337fecad8014acffe3aa5",
+1
-4
src/widgets.zig
+1
-4
src/widgets.zig
···
11
11
pub const TextView = @import("widgets/TextView.zig");
12
12
pub const CodeView = @import("widgets/CodeView.zig");
13
13
pub const Terminal = @import("widgets/terminal/Terminal.zig");
14
-
15
-
// Widgets with dependencies
16
-
17
-
pub const TextInput = if (opts.text_input) @import("widgets/TextInput.zig") else undefined;
14
+
pub const TextInput = @import("widgets/TextInput.zig");
+196
-125
src/widgets/TextInput.zig
+196
-125
src/widgets/TextInput.zig
···
3
3
const Key = @import("../Key.zig");
4
4
const Cell = @import("../Cell.zig");
5
5
const Window = @import("../Window.zig");
6
-
const GapBuffer = @import("gap_buffer").GapBuffer;
7
6
const Unicode = @import("../Unicode.zig");
8
7
9
8
const TextInput = @This();
···
18
17
// Index of our cursor
19
18
cursor_idx: usize = 0,
20
19
grapheme_count: usize = 0,
21
-
buf: GapBuffer(u8),
20
+
buf: Buffer,
22
21
23
22
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
24
23
draw_offset: usize = 0,
···
33
32
34
33
pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput {
35
34
return TextInput{
36
-
.buf = GapBuffer(u8).init(alloc),
35
+
.buf = Buffer.init(alloc),
37
36
.unicode = unicode,
38
37
};
39
38
}
···
47
46
.key_press => |key| {
48
47
if (key.matches(Key.backspace, .{})) {
49
48
if (self.cursor_idx == 0) return;
50
-
try self.deleteBeforeCursor();
49
+
self.deleteBeforeCursor();
51
50
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
52
-
if (self.cursor_idx == self.grapheme_count) return;
53
-
try self.deleteAtCursor();
51
+
self.deleteAfterCursor();
54
52
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
55
-
if (self.cursor_idx > 0) self.cursor_idx -= 1;
53
+
self.cursorLeft();
56
54
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
57
-
if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1;
55
+
self.cursorRight();
58
56
} else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) {
57
+
self.buf.moveGapLeft(self.buf.firstHalf().len);
59
58
self.cursor_idx = 0;
60
59
} else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) {
60
+
self.buf.moveGapRight(self.buf.secondHalf().len);
61
61
self.cursor_idx = self.grapheme_count;
62
62
} else if (key.matches('k', .{ .ctrl = true })) {
63
-
try self.deleteToEnd();
63
+
self.deleteToEnd();
64
64
} else if (key.matches('u', .{ .ctrl = true })) {
65
-
try self.deleteToStart();
65
+
self.deleteToStart();
66
66
} else if (key.text) |text| {
67
-
try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text);
68
-
self.cursor_idx += 1;
69
-
self.grapheme_count += 1;
67
+
try self.insertSliceAtCursor(text);
70
68
}
71
69
},
72
70
}
73
71
}
74
72
75
73
/// insert text at the cursor position
76
-
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void {
74
+
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void {
77
75
var iter = self.unicode.graphemeIterator(data);
78
76
var byte_offset_to_cursor = self.byteOffsetToCursor();
79
77
while (iter.next()) |text| {
80
-
try self.buf.insertSliceBefore(byte_offset_to_cursor, text.bytes(data));
78
+
try self.buf.insertSliceAtCursor(text.bytes(data));
81
79
byte_offset_to_cursor += text.len;
82
80
self.cursor_idx += 1;
83
81
self.grapheme_count += 1;
···
85
83
}
86
84
87
85
pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 {
88
-
const offset = self.byteOffsetToCursor();
89
-
assert(offset <= buf.len); // provided buf was too small
90
-
91
-
if (offset <= self.buf.items.len) {
92
-
@memcpy(buf[0..offset], self.buf.items[0..offset]);
93
-
} else {
94
-
@memcpy(buf[0..self.buf.items.len], self.buf.items);
95
-
const second_half = self.buf.secondHalf();
96
-
const copy_len = offset - self.buf.items.len;
97
-
@memcpy(buf[self.buf.items.len .. self.buf.items.len + copy_len], second_half[0..copy_len]);
98
-
}
99
-
return buf[0..offset];
86
+
assert(buf.len >= self.buf.cursor);
87
+
@memcpy(buf[0..self.buf.cursor], self.buf.firstHalf());
88
+
return buf[0..self.buf.cursor];
100
89
}
101
90
102
91
/// calculates the display width from the draw_offset to the cursor
103
92
fn widthToCursor(self: *TextInput, win: Window) usize {
104
93
var width: usize = 0;
105
-
var first_iter = self.unicode.graphemeIterator(self.buf.items);
94
+
const first_half = self.buf.firstHalf();
95
+
var first_iter = self.unicode.graphemeIterator(first_half);
106
96
var i: usize = 0;
107
97
while (first_iter.next()) |grapheme| {
108
98
defer i += 1;
···
110
100
continue;
111
101
}
112
102
if (i == self.cursor_idx) return width;
113
-
const g = grapheme.bytes(self.buf.items);
114
-
width += win.gwidth(g);
115
-
}
116
-
const second_half = self.buf.secondHalf();
117
-
var second_iter = self.unicode.graphemeIterator(second_half);
118
-
while (second_iter.next()) |grapheme| {
119
-
defer i += 1;
120
-
if (i < self.draw_offset) {
121
-
continue;
122
-
}
123
-
if (i == self.cursor_idx) return width;
124
-
const g = grapheme.bytes(second_half);
103
+
const g = grapheme.bytes(first_half);
125
104
width += win.gwidth(g);
126
105
}
127
106
return width;
128
107
}
129
108
109
+
fn cursorLeft(self: *TextInput) void {
110
+
if (self.cursor_idx == 0) return;
111
+
// We need to find the size of the last grapheme in the first half
112
+
var iter = self.unicode.graphemeIterator(self.buf.firstHalf());
113
+
var len: usize = 0;
114
+
while (iter.next()) |grapheme| {
115
+
len = grapheme.len;
116
+
}
117
+
self.buf.moveGapLeft(len);
118
+
self.cursor_idx -= 1;
119
+
}
120
+
121
+
fn cursorRight(self: *TextInput) void {
122
+
if (self.cursor_idx >= self.grapheme_count) return;
123
+
var iter = self.unicode.graphemeIterator(self.buf.secondHalf());
124
+
const grapheme = iter.next() orelse return;
125
+
self.buf.moveGapRight(grapheme.len);
126
+
self.cursor_idx += 1;
127
+
}
128
+
130
129
pub fn draw(self: *TextInput, win: Window) void {
131
130
if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx;
132
131
if (win.width == 0) return;
···
143
142
144
143
// assumption!! the gap is never within a grapheme
145
144
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
146
-
var first_iter = self.unicode.graphemeIterator(self.buf.items);
145
+
const first_half = self.buf.firstHalf();
146
+
var first_iter = self.unicode.graphemeIterator(first_half);
147
147
var col: usize = 0;
148
148
var i: usize = 0;
149
149
while (first_iter.next()) |grapheme| {
···
151
151
i += 1;
152
152
continue;
153
153
}
154
-
const g = grapheme.bytes(self.buf.items);
154
+
const g = grapheme.bytes(first_half);
155
155
const w = win.gwidth(g);
156
156
if (col + w >= win.width) {
157
157
win.writeCell(win.width - 1, 0, .{ .char = ellipsis });
···
220
220
}
221
221
222
222
// returns the number of bytes before the cursor
223
-
// (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength()
224
-
// which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr)
225
223
pub fn byteOffsetToCursor(self: TextInput) usize {
226
-
// assumption! the gap is never in the middle of a grapheme
227
-
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
228
-
var iter = self.unicode.graphemeIterator(self.buf.items);
229
-
var offset: usize = 0;
230
-
var i: usize = 0;
231
-
while (iter.next()) |grapheme| {
232
-
if (i == self.cursor_idx) break;
233
-
offset += grapheme.len;
234
-
i += 1;
235
-
} else {
236
-
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
237
-
while (second_iter.next()) |grapheme| {
238
-
if (i == self.cursor_idx) break;
239
-
offset += grapheme.len;
240
-
i += 1;
241
-
}
242
-
}
243
-
return offset;
224
+
return self.buf.cursor;
244
225
}
245
226
246
-
fn deleteToEnd(self: *TextInput) !void {
247
-
const offset = self.byteOffsetToCursor();
248
-
try self.buf.replaceRangeAfter(offset, self.buf.realLength() - offset, &.{});
227
+
fn deleteToEnd(self: *TextInput) void {
228
+
self.buf.growGapRight(self.buf.secondHalf().len);
249
229
self.grapheme_count = self.cursor_idx;
250
230
}
251
231
252
-
fn deleteToStart(self: *TextInput) !void {
253
-
const offset = self.byteOffsetToCursor();
254
-
try self.buf.replaceRangeBefore(0, offset, &.{});
232
+
fn deleteToStart(self: *TextInput) void {
233
+
self.buf.growGapLeft(self.buf.cursor);
255
234
self.grapheme_count -= self.cursor_idx;
256
235
self.cursor_idx = 0;
257
236
}
258
237
259
-
fn deleteBeforeCursor(self: *TextInput) !void {
260
-
// assumption! the gap is never in the middle of a grapheme
261
-
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
262
-
var iter = self.unicode.graphemeIterator(self.buf.items);
263
-
var offset: usize = 0;
264
-
var i: usize = 1;
238
+
fn deleteBeforeCursor(self: *TextInput) void {
239
+
if (self.cursor_idx == 0) return;
240
+
// We need to find the size of the last grapheme in the first half
241
+
var iter = self.unicode.graphemeIterator(self.buf.firstHalf());
242
+
var len: usize = 0;
265
243
while (iter.next()) |grapheme| {
266
-
if (i == self.cursor_idx) {
267
-
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
268
-
self.cursor_idx -= 1;
269
-
self.grapheme_count -= 1;
270
-
return;
271
-
}
272
-
offset += grapheme.len;
273
-
i += 1;
274
-
} else {
275
-
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
276
-
while (second_iter.next()) |grapheme| {
277
-
if (i == self.cursor_idx) {
278
-
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
279
-
self.cursor_idx -= 1;
280
-
self.grapheme_count -= 1;
281
-
return;
282
-
}
283
-
offset += grapheme.len;
284
-
i += 1;
285
-
}
244
+
len = grapheme.len;
286
245
}
246
+
self.buf.growGapLeft(len);
247
+
self.cursor_idx -= 1;
248
+
self.grapheme_count -= 1;
287
249
}
288
250
289
-
fn deleteAtCursor(self: *TextInput) !void {
290
-
// assumption! the gap is never in the middle of a grapheme
291
-
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
292
-
var iter = self.unicode.graphemeIterator(self.buf.items);
293
-
var offset: usize = 0;
294
-
var i: usize = 1;
295
-
while (iter.next()) |grapheme| {
296
-
if (i == self.cursor_idx + 1) {
297
-
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
298
-
self.grapheme_count -= 1;
299
-
return;
300
-
}
301
-
offset += grapheme.len;
302
-
i += 1;
303
-
} else {
304
-
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
305
-
while (second_iter.next()) |grapheme| {
306
-
if (i == self.cursor_idx + 1) {
307
-
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
308
-
self.grapheme_count -= 1;
309
-
return;
310
-
}
311
-
offset += grapheme.len;
312
-
i += 1;
313
-
}
314
-
}
251
+
fn deleteAfterCursor(self: *TextInput) void {
252
+
if (self.cursor_idx == self.grapheme_count) return;
253
+
var iter = self.unicode.graphemeIterator(self.buf.secondHalf());
254
+
const grapheme = iter.next() orelse return;
255
+
self.buf.growGapRight(grapheme.len);
256
+
self.grapheme_count -= 1;
315
257
}
316
258
317
259
test "assertion" {
···
337
279
var input = init(alloc, &unicode);
338
280
defer input.deinit();
339
281
try input.insertSliceAtCursor("hello, world");
340
-
input.cursor_idx = 2;
282
+
input.cursorLeft();
283
+
input.cursorLeft();
284
+
input.cursorLeft();
341
285
var buf: [32]u8 = undefined;
342
-
try std.testing.expectEqualStrings("he", input.sliceToCursor(&buf));
343
-
input.buf.moveGap(3);
344
-
input.cursor_idx = 5;
345
-
try std.testing.expectEqualStrings("hello", input.sliceToCursor(&buf));
286
+
try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf));
287
+
input.cursorRight();
288
+
try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf));
289
+
}
290
+
291
+
const Buffer = struct {
292
+
allocator: std.mem.Allocator,
293
+
buffer: []u8,
294
+
cursor: usize,
295
+
gap_size: usize,
296
+
297
+
fn init(allocator: std.mem.Allocator) Buffer {
298
+
return .{
299
+
.allocator = allocator,
300
+
.buffer = &.{},
301
+
.cursor = 0,
302
+
.gap_size = 0,
303
+
};
304
+
}
305
+
306
+
fn deinit(self: *Buffer) void {
307
+
self.allocator.free(self.buffer);
308
+
}
309
+
310
+
fn firstHalf(self: Buffer) []const u8 {
311
+
return self.buffer[0..self.cursor];
312
+
}
313
+
314
+
fn secondHalf(self: Buffer) []const u8 {
315
+
return self.buffer[self.cursor + self.gap_size ..];
316
+
}
317
+
318
+
fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void {
319
+
// Always grow by 512 bytes
320
+
const new_size = self.buffer.len + n + 512;
321
+
// Allocate the new memory
322
+
const new_memory = try self.allocator.alloc(u8, new_size);
323
+
// Copy the first half
324
+
@memcpy(new_memory[0..self.cursor], self.firstHalf());
325
+
// Copy the second half
326
+
const second_half = self.secondHalf();
327
+
@memcpy(new_memory[new_size - second_half.len ..], second_half);
328
+
self.allocator.free(self.buffer);
329
+
self.buffer = new_memory;
330
+
self.gap_size = new_size - second_half.len - self.cursor;
331
+
}
332
+
333
+
fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void {
334
+
if (slice.len == 0) return;
335
+
if (self.gap_size <= slice.len) try self.grow(slice.len);
336
+
@memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice);
337
+
self.cursor += slice.len;
338
+
self.gap_size -= slice.len;
339
+
}
340
+
341
+
/// Move the gap n bytes to the left
342
+
fn moveGapLeft(self: *Buffer, n: usize) void {
343
+
const new_idx = self.cursor -| n;
344
+
const dst = self.buffer[new_idx + self.gap_size ..];
345
+
const src = self.buffer[new_idx..self.cursor];
346
+
std.mem.copyForwards(u8, dst, src);
347
+
self.cursor = new_idx;
348
+
}
349
+
350
+
fn moveGapRight(self: *Buffer, n: usize) void {
351
+
const new_idx = self.cursor + n;
352
+
const dst = self.buffer[self.cursor..];
353
+
const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size];
354
+
std.mem.copyForwards(u8, dst, src);
355
+
self.cursor = new_idx;
356
+
}
357
+
358
+
/// grow the gap by moving the cursor n bytes to the left
359
+
fn growGapLeft(self: *Buffer, n: usize) void {
360
+
// gap grows by the delta
361
+
self.gap_size += n;
362
+
self.cursor -|= n;
363
+
}
364
+
365
+
/// grow the gap by removing n bytes after the cursor
366
+
fn growGapRight(self: *Buffer, n: usize) void {
367
+
self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor);
368
+
}
369
+
370
+
fn clearAndFree(self: *Buffer) void {
371
+
self.cursor = 0;
372
+
self.allocator.free(self.buffer);
373
+
self.buffer = &.{};
374
+
self.gap_size = 0;
375
+
}
376
+
377
+
fn clearRetainingCapacity(self: *Buffer) void {
378
+
self.cursor = 0;
379
+
self.gap_size = self.buffer.len;
380
+
}
381
+
382
+
fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 {
383
+
const first_half = self.firstHalf();
384
+
const second_half = self.secondHalf();
385
+
const buf = try self.allocator.alloc(u8, first_half.len + second_half.len);
386
+
@memcpy(buf[0..first_half.len], first_half);
387
+
@memcpy(buf[first_half.len..], second_half);
388
+
self.clearAndFree();
389
+
}
390
+
};
391
+
392
+
test "TextInput.zig: Buffer" {
393
+
var gap_buf = Buffer.init(std.testing.allocator);
394
+
defer gap_buf.deinit();
395
+
396
+
try gap_buf.insertSliceAtCursor("abc");
397
+
try std.testing.expectEqualStrings("abc", gap_buf.firstHalf());
398
+
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
399
+
400
+
gap_buf.moveGapLeft(1);
401
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
402
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
403
+
404
+
try gap_buf.insertSliceAtCursor(" ");
405
+
try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf());
406
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
407
+
408
+
gap_buf.growGapLeft(1);
409
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
410
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
411
+
try std.testing.expectEqual(2, gap_buf.cursor);
412
+
413
+
gap_buf.growGapRight(1);
414
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
415
+
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
416
+
try std.testing.expectEqual(2, gap_buf.cursor);
346
417
}