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 }