lib/xkb/src/compose/state.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const xkb = @import("../root.zig");
3 const compose = @import("root.zig");
4
5 const Keysym = xkb.keysym.Keysym;
6
7 pub const Status = enum {
8 nothing,
9 composing,
10 composed,
11 cancelled,
12 };
13
14 pub const FeedResult = enum {
15 ignored,
16 accepted,
17 };
18
19 pub const TextError = error{BufferTooSmall};
20
21 pub const State = struct {
22 table: compose.View,
23 previous: ?u32 = 0,
24 current: ?u32 = 0,
25
26 pub fn init(table: *const compose.Table) State {
27 return .{ .table = table.view() };
28 }
29
30 pub fn reset(self: *State) void {
31 self.previous = 0;
32 self.current = 0;
33 }
34
35 pub fn feed(self: *State, input_symbol: Keysym) FeedResult {
36 if (xkb.keysym.isModifier(input_symbol)) return .ignored;
37
38 const start = if (self.current) |node|
39 if (node == 0 or self.table.result(node) == null) node else 0
40 else
41 0;
42 self.previous = self.current;
43 self.current = self.table.transition(start, input_symbol);
44 return .accepted;
45 }
46
47 pub fn status(self: *const State) Status {
48 const current = self.current orelse {
49 const previous = self.previous orelse return .nothing;
50 if (previous != 0 and self.table.result(previous) == null) {
51 return .cancelled;
52 }
53 return .nothing;
54 };
55 if (current == 0) return .nothing;
56 return if (self.table.result(current) == null) .composing else .composed;
57 }
58
59 pub fn symbol(self: *const State) ?Keysym {
60 const current = self.current orelse return null;
61 const result_value = self.table.result(current) orelse return null;
62 return result_value.symbol;
63 }
64
65 pub fn writeUtf8(self: *const State, buffer: []u8) TextError!?[]const u8 {
66 const current = self.current orelse return null;
67 const result_value = self.table.result(current) orelse return null;
68 if (result_value.text) |text| {
69 if (text.len > buffer.len) return error.BufferTooSmall;
70 @memcpy(buffer[0..text.len], text);
71 return buffer[0..text.len];
72 }
73 const symbol_value = result_value.symbol orelse return null;
74 const codepoint = xkb.keysym.codepoint(symbol_value) orelse return null;
75 var encoded: [4]u8 = undefined;
76 const length = std.unicode.utf8Encode(codepoint, &encoded) catch return null;
77 if (length > buffer.len) return error.BufferTooSmall;
78 @memcpy(buffer[0..length], encoded[0..length]);
79 return buffer[0..length];
80 }
81 };
82
83 test "state distinguishes composing composed cancelled and fresh input" {
84 const allocator = std.testing.allocator;
85 var storage = try compose.Storage.init(allocator, .{
86 .sequence_symbols = 8,
87 .text_bytes = 8,
88 });
89 defer storage.deinit(allocator);
90 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
91 defer scratch_storage.deinit(allocator);
92 storage.activate();
93 scratch_storage.activate();
94 const table = try compose.compile(
95 &storage,
96 &scratch_storage,
97 \\<Multi_key> <a> : "å" aring
98 \\<A> : dollar
99 ,
100 .{ .locale = "C" },
101 );
102 defer storage.reset();
103 var state = State.init(&table);
104
105 const multi: Keysym = @fromBackingInt(@intCast(0xff20));
106 const a: Keysym = @fromBackingInt(@intCast('a'));
107 const upper_a: Keysym = @fromBackingInt(@intCast('A'));
108 const unknown: Keysym = @fromBackingInt(@intCast('7'));
109 try std.testing.expectEqual(Status.nothing, state.status());
110 try std.testing.expectEqual(FeedResult.accepted, state.feed(multi));
111 try std.testing.expectEqual(Status.composing, state.status());
112 try std.testing.expectEqual(FeedResult.ignored, state.feed(@fromBackingInt(@intCast(0xffe1))));
113 try std.testing.expectEqual(Status.composing, state.status());
114 try std.testing.expectEqual(FeedResult.accepted, state.feed(a));
115 try std.testing.expectEqual(Status.composed, state.status());
116 try std.testing.expectEqual(@as(?Keysym, @fromBackingInt(@intCast(0x00e5))), state.symbol());
117 var buffer: [8]u8 = undefined;
118 try std.testing.expectEqualStrings("å", (try state.writeUtf8(&buffer)).?);
119
120 _ = state.feed(unknown);
121 try std.testing.expectEqual(Status.nothing, state.status());
122 _ = state.feed(multi);
123 _ = state.feed(unknown);
124 try std.testing.expectEqual(Status.cancelled, state.status());
125 _ = state.feed(upper_a);
126 try std.testing.expectEqual(Status.composed, state.status());
127 try std.testing.expectEqualStrings("$", (try state.writeUtf8(&buffer)).?);
128 state.reset();
129 try std.testing.expectEqual(Status.nothing, state.status());
130 }
131
132 test "keysym-only results derive UTF-8 and enforce caller capacity" {
133 const allocator = std.testing.allocator;
134 var storage = try compose.Storage.init(allocator, .{
135 .sequence_symbols = 1,
136 .text_bytes = 0,
137 });
138 defer storage.deinit(allocator);
139 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
140 defer scratch_storage.deinit(allocator);
141 storage.activate();
142 scratch_storage.activate();
143 const table = try compose.compile(
144 &storage,
145 &scratch_storage,
146 "<A> : U1F600\n",
147 .{ .locale = "C" },
148 );
149 defer storage.reset();
150 var state = State.init(&table);
151 _ = state.feed(@fromBackingInt(@intCast('A')));
152 var short: [3]u8 = undefined;
153 try std.testing.expectError(error.BufferTooSmall, state.writeUtf8(&short));
154 var enough: [4]u8 = undefined;
155 try std.testing.expectEqualStrings("😀", (try state.writeUtf8(&enough)).?);
156 }