lib/xkb/src/state/model.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const xkb = @import("../root.zig");
  3 
  4 const keymap = xkb.keymap;
  5 const keysym = xkb.keysym;
  6 
  7 pub const Mask = struct {
  8     depressed_modifiers: keymap.ModifierMask = 0,
  9     latched_modifiers: keymap.ModifierMask = 0,
 10     locked_modifiers: keymap.ModifierMask = 0,
 11     depressed_layout: u32 = 0,
 12     latched_layout: u32 = 0,
 13     locked_layout: u32 = 0,
 14 };
 15 
 16 pub const Changes = struct {
 17     depressed_modifiers: bool = false,
 18     latched_modifiers: bool = false,
 19     locked_modifiers: bool = false,
 20     effective_modifiers: bool = false,
 21     depressed_layout: bool = false,
 22     latched_layout: bool = false,
 23     locked_layout: bool = false,
 24     effective_layout: bool = false,
 25 
 26     pub fn any(self: Changes) bool {
 27         return self.depressed_modifiers or self.latched_modifiers or
 28             self.locked_modifiers or self.effective_modifiers or
 29             self.depressed_layout or self.latched_layout or
 30             self.locked_layout or self.effective_layout;
 31     }
 32 };
 33 
 34 pub const ModifierComponents = struct {
 35     depressed: bool = false,
 36     latched: bool = false,
 37     locked: bool = false,
 38     effective: bool = false,
 39 };
 40 
 41 pub const TextError = error{BufferTooSmall};
 42 
 43 const Components = struct {
 44     depressed_modifiers: keymap.ModifierMask = 0,
 45     latched_modifiers: keymap.ModifierMask = 0,
 46     locked_modifiers: keymap.ModifierMask = 0,
 47     effective_modifiers: keymap.ModifierMask = 0,
 48     depressed_layout: i32 = 0,
 49     latched_layout: i32 = 0,
 50     locked_layout: i32 = 0,
 51     effective_layout: i32 = 0,
 52 };
 53 
 54 pub const State = struct {
 55     keymap: *const keymap.Keymap,
 56     components: Components = .{},
 57 
 58     pub fn init(compiled: *const keymap.Keymap) State {
 59         return .{ .keymap = compiled };
 60     }
 61 
 62     pub fn update(self: *State, input: Mask) Changes {
 63         const previous = self.components;
 64         self.components.depressed_modifiers = self.keymap.resolveToCanonicalModifiers(input.depressed_modifiers);
 65         self.components.latched_modifiers = self.keymap.resolveToCanonicalModifiers(input.latched_modifiers);
 66         self.components.locked_modifiers = self.keymap.resolveToCanonicalModifiers(input.locked_modifiers);
 67         self.components.effective_modifiers = self.components.depressed_modifiers |
 68             self.components.latched_modifiers |
 69             self.components.locked_modifiers;
 70         self.components.depressed_layout = @bitCast(input.depressed_layout);
 71         self.components.latched_layout = @bitCast(input.latched_layout);
 72         self.components.locked_layout = layoutInRange(@bitCast(input.locked_layout), self.keymap.layoutCount());
 73         const layout_sum = self.components.depressed_layout +%
 74             self.components.latched_layout +% self.components.locked_layout;
 75         self.components.effective_layout = layoutInRange(layout_sum, self.keymap.layoutCount());
 76         return changes(previous, self.components);
 77     }
 78 
 79     pub fn effectiveModifiers(self: *const State) keymap.ModifierMask {
 80         return self.components.effective_modifiers;
 81     }
 82 
 83     pub fn effectiveLayout(self: *const State) usize {
 84         return @intCast(self.components.effective_layout);
 85     }
 86 
 87     pub fn modifierActive(self: *const State, name: []const u8) bool {
 88         const index = self.keymap.modifierIndex(name) orelse return false;
 89         return self.modifierIndexActive(index, .{ .effective = true });
 90     }
 91 
 92     pub fn modifierIndexActive(
 93         self: *const State,
 94         index: usize,
 95         requested: ModifierComponents,
 96     ) bool {
 97         const mapping = self.keymap.modifierMapping(index) orelse return false;
 98         if (mapping == 0) return false;
 99         const active = self.serializeModifiers(requested);
100         return active & mapping == mapping;
101     }
102 
103     pub fn keyRepeats(self: *const State, code: keymap.Keycode) bool {
104         const key_value = self.keymap.key(code) orelse return false;
105         return key_value.repeats;
106     }
107 
108     pub fn layout(self: *const State, code: keymap.Keycode) ?usize {
109         const key_value = self.keymap.key(code) orelse return null;
110         return self.keymap.group(key_value, self.components.effective_layout);
111     }
112 
113     pub fn level(self: *const State, code: keymap.Keycode) ?usize {
114         const key_value = self.keymap.key(code) orelse return null;
115         const group_index = self.keymap.group(key_value, self.components.effective_layout) orelse return null;
116         return self.levelForGroup(key_value, group_index);
117     }
118 
119     pub fn symbols(self: *const State, code: keymap.Keycode) []const keysym.Keysym {
120         const selection_value = self.selection(code) orelse return &.{};
121         return selection_value.level.resolvedSymbols(self.capsTransforms(code));
122     }
123 
124     pub fn oneSymbol(self: *const State, code: keymap.Keycode) ?keysym.Keysym {
125         const selected = self.symbols(code);
126         if (selected.len != 1) return null;
127         return selected[0];
128     }
129 
130     pub fn consumedModifiers(self: *const State, code: keymap.Keycode) keymap.ModifierMask {
131         const selection_value = self.selection(code) orelse return 0;
132         const key_type = self.keymap.keyType(selection_value.group.type_index).?;
133         const active = self.components.effective_modifiers & key_type.modifiers;
134         var preserve: keymap.ModifierMask = 0;
135         for (key_type.entries) |entry| {
136             if (entry.modifiers == active) {
137                 preserve = entry.preserve;
138                 break;
139             }
140         }
141         return key_type.modifiers & ~preserve;
142     }
143 
144     pub fn modifierConsumed(self: *const State, code: keymap.Keycode, name: []const u8) bool {
145         const index = self.keymap.modifierIndex(name) orelse return false;
146         const mapping = self.keymap.modifierMapping(index) orelse return false;
147         if (mapping == 0) return false;
148         return self.consumedModifiers(code) & mapping == mapping;
149     }
150 
151     pub fn writeUtf8(
152         self: *const State,
153         code: keymap.Keycode,
154         buffer: []u8,
155     ) TextError!?[]const u8 {
156         const control = self.controlTransforms(code);
157         if (self.oneSymbolForText(code, control)) |symbol| {
158             const written = try writeSymbol(symbol, buffer);
159             if (written == 0) return null;
160             if (written == 1 and control) buffer[0] = toControl(buffer[0]);
161             return buffer[0..written];
162         }
163 
164         const selected = self.symbols(code);
165         if (selected.len == 0) return null;
166         var written: usize = 0;
167         for (selected) |symbol| {
168             const count = try writeSymbol(symbol, buffer[written..]);
169             if (count == 0) return null;
170             written += count;
171         }
172         if (written == 1 and control) buffer[0] = toControl(buffer[0]);
173         return buffer[0..written];
174     }
175 
176     fn serializeModifiers(self: *const State, requested: ModifierComponents) keymap.ModifierMask {
177         if (requested.effective) return self.components.effective_modifiers;
178         var result: keymap.ModifierMask = 0;
179         if (requested.depressed) result |= self.components.depressed_modifiers;
180         if (requested.latched) result |= self.components.latched_modifiers;
181         if (requested.locked) result |= self.components.locked_modifiers;
182         return result;
183     }
184 
185     fn selection(self: *const State, code: keymap.Keycode) ?Selection {
186         const key_value = self.keymap.key(code) orelse return null;
187         const group_index = self.keymap.group(key_value, self.components.effective_layout) orelse return null;
188         const group = &key_value.groups[group_index];
189         const level_index = self.levelForGroup(key_value, group_index);
190         if (level_index >= group.levels.len) return null;
191         return .{
192             .key = key_value,
193             .group = group,
194             .level = &group.levels[level_index],
195         };
196     }
197 
198     fn levelForGroup(self: *const State, key_value: *const keymap.Key, group_index: usize) usize {
199         const group = key_value.groups[group_index];
200         const key_type = self.keymap.keyType(group.type_index).?;
201         const active = self.components.effective_modifiers & key_type.modifiers;
202         for (key_type.entries) |entry| {
203             if (entry.modifiers == active) return entry.level;
204         }
205         return 0;
206     }
207 
208     fn capsTransforms(self: *const State, code: keymap.Keycode) bool {
209         return self.modifierActive("Lock") and !self.modifierConsumed(code, "Lock");
210     }
211 
212     fn controlTransforms(self: *const State, code: keymap.Keycode) bool {
213         return self.modifierActive("Control") and !self.modifierConsumed(code, "Control");
214     }
215 
216     fn oneSymbolForText(
217         self: *const State,
218         code: keymap.Keycode,
219         control: bool,
220     ) ?keysym.Keysym {
221         const selected = self.selection(code) orelse return null;
222         if (selected.level.symbols.len != 1) return null;
223         var selected_level = selected.level;
224         if (control and @backingInt(selected_level.symbols[0]) > 127) {
225             for (selected.key.groups, 0..) |group, group_index| {
226                 const level_index = self.levelForGroup(selected.key, group_index);
227                 if (level_index >= group.levels.len) continue;
228                 const candidates = group.levels[level_index].symbols;
229                 if (candidates.len == 1 and @backingInt(candidates[0]) <= 127) {
230                     selected_level = &group.levels[level_index];
231                     break;
232                 }
233             }
234         }
235         const resolved = selected_level.resolvedSymbols(self.capsTransforms(code));
236         return resolved[0];
237     }
238 };
239 
240 const Selection = struct {
241     key: *const keymap.Key,
242     group: *const keymap.Group,
243     level: *const keymap.Level,
244 };
245 
246 fn layoutInRange(value: i32, count: usize) i32 {
247     if (count == 0) return 0;
248     return @intCast(@mod(@as(i64, value), @as(i64, @intCast(count))));
249 }
250 
251 fn changes(previous: Components, next: Components) Changes {
252     return .{
253         .depressed_modifiers = previous.depressed_modifiers != next.depressed_modifiers,
254         .latched_modifiers = previous.latched_modifiers != next.latched_modifiers,
255         .locked_modifiers = previous.locked_modifiers != next.locked_modifiers,
256         .effective_modifiers = previous.effective_modifiers != next.effective_modifiers,
257         .depressed_layout = previous.depressed_layout != next.depressed_layout,
258         .latched_layout = previous.latched_layout != next.latched_layout,
259         .locked_layout = previous.locked_layout != next.locked_layout,
260         .effective_layout = previous.effective_layout != next.effective_layout,
261     };
262 }
263 
264 fn writeSymbol(symbol: keysym.Keysym, buffer: []u8) TextError!usize {
265     const codepoint = keysym.codepoint(symbol) orelse return 0;
266     var encoded: [4]u8 = undefined;
267     const count = std.unicode.utf8Encode(codepoint, &encoded) catch return 0;
268     if (count > buffer.len) return error.BufferTooSmall;
269     @memcpy(buffer[0..count], encoded[0..count]);
270     return count;
271 }
272 
273 fn toControl(input: u8) u8 {
274     if ((input >= '@' and input < 0x7f) or input == ' ') return input & 0x1f;
275     if (input == '2') return 0;
276     if (input >= '3' and input <= '7') return input - ('3' - 0x1b);
277     if (input == '8') return 0x7f;
278     if (input == '/') return '_' & 0x1f;
279     return input;
280 }
281 
282 const Fixture = struct {
283     const modifiers = keymap.core_modifiers ++ [_]keymap.Modifier{
284         .{ .name = "NumLock", .mapping = 1 << 3, .kind = .virtual },
285     };
286     const alphabet_entries = [_]keymap.TypeEntry{
287         .{ .modifiers = 1 << 0, .level = 1 },
288         .{ .modifiers = 1 << 1, .level = 0, .preserve = 1 << 1 },
289         .{ .modifiers = (1 << 0) | (1 << 1), .level = 1, .preserve = 1 << 1 },
290     };
291     const types = [_]keymap.KeyType{
292         .{ .modifiers = (1 << 0) | (1 << 1), .level_count = 2, .entries = &alphabet_entries },
293         .{ .modifiers = 0, .level_count = 1, .entries = &.{} },
294     };
295     const a_levels = [_]keymap.Level{
296         .{ .symbols = &.{@fromBackingInt(@intCast('a'))}, .uppercase_symbols = &.{@fromBackingInt(@intCast('A'))} },
297         .{ .symbols = &.{@fromBackingInt(@intCast('A'))} },
298     };
299     const a_groups = [_]keymap.Group{.{ .type_index = 0, .levels = &a_levels }};
300     const multi_levels = [_]keymap.Level{.{
301         .symbols = &.{ @fromBackingInt(@intCast('x')), @fromBackingInt(@intCast('y')) },
302         .uppercase_symbols = &.{ @fromBackingInt(@intCast('X')), @fromBackingInt(@intCast('Y')) },
303     }};
304     const multi_groups = [_]keymap.Group{.{ .type_index = 1, .levels = &multi_levels }};
305     const unicode_levels = [_]keymap.Level{.{
306         .symbols = &.{@fromBackingInt(@intCast(0x010003bb))},
307         .uppercase_symbols = &.{@fromBackingInt(@intCast(0x0100039b))},
308     }};
309     const ascii_levels = [_]keymap.Level{.{
310         .symbols = &.{@fromBackingInt(@intCast('l'))},
311         .uppercase_symbols = &.{@fromBackingInt(@intCast('L'))},
312     }};
313     const fallback_groups = [_]keymap.Group{
314         .{ .type_index = 1, .levels = &unicode_levels },
315         .{ .type_index = 1, .levels = &ascii_levels },
316     };
317     const keys = [_]keymap.Key{
318         .{ .code = 38, .groups = &a_groups, .repeats = true },
319         .{ .code = 39, .groups = &multi_groups },
320         .{ .code = 40, .groups = &fallback_groups },
321     };
322 
323     fn definition() keymap.Definition {
324         return .{
325             .modifiers = &modifiers,
326             .types = &types,
327             .keys = &keys,
328         };
329     }
330 };
331 
332 test "external masks select levels and report state changes" {
333     var storage: keymap.Keymap.Storage = undefined;
334     var state = State.init(try keymap.Keymap.init(&storage, Fixture.definition()));
335 
336     try std.testing.expectEqual(@as(?keysym.Keysym, @fromBackingInt(@intCast('a'))), state.oneSymbol(38));
337     const first = state.update(.{ .depressed_modifiers = 1 });
338     try std.testing.expect(first.depressed_modifiers);
339     try std.testing.expect(first.effective_modifiers);
340     try std.testing.expectEqual(@as(?keysym.Keysym, @fromBackingInt(@intCast('A'))), state.oneSymbol(38));
341     try std.testing.expect(!state.update(.{ .depressed_modifiers = 1 }).any());
342 
343     const virtual_num_lock = keymap.modifierBit(8);
344     _ = state.update(.{ .locked_modifiers = virtual_num_lock });
345     try std.testing.expect(state.modifierActive("Mod1"));
346     try std.testing.expect(state.modifierActive("NumLock"));
347 }
348 
349 test "caps preserve and control transformations match XKB text semantics" {
350     var storage: keymap.Keymap.Storage = undefined;
351     var state = State.init(try keymap.Keymap.init(&storage, Fixture.definition()));
352     var buffer: [8]u8 = undefined;
353 
354     _ = state.update(.{ .locked_modifiers = 1 << 1 });
355     try std.testing.expectEqual(@as(?keysym.Keysym, @fromBackingInt(@intCast('A'))), state.oneSymbol(38));
356     try std.testing.expectEqualStrings("A", (try state.writeUtf8(38, &buffer)).?);
357 
358     _ = state.update(.{ .depressed_modifiers = 1 << 2 });
359     const control_a = (try state.writeUtf8(38, &buffer)).?;
360     try std.testing.expectEqual(@as(usize, 1), control_a.len);
361     try std.testing.expectEqual(@as(u8, 1), control_a[0]);
362 
363     _ = state.update(.{ .depressed_modifiers = 1 << 2, .depressed_layout = 0 });
364     const fallback = (try state.writeUtf8(40, &buffer)).?;
365     try std.testing.expectEqual(@as(usize, 1), fallback.len);
366     try std.testing.expectEqual(@as(u8, 12), fallback[0]);
367 }
368 
369 test "multi-symbol levels concatenate Unicode and preserve one-symbol absence" {
370     var storage: keymap.Keymap.Storage = undefined;
371     var state = State.init(try keymap.Keymap.init(&storage, Fixture.definition()));
372     var buffer: [8]u8 = undefined;
373 
374     try std.testing.expectEqual(@as(?keysym.Keysym, null), state.oneSymbol(39));
375     try std.testing.expectEqualStrings("xy", (try state.writeUtf8(39, &buffer)).?);
376     try std.testing.expectError(error.BufferTooSmall, state.writeUtf8(39, buffer[0..1]));
377     try std.testing.expect(state.keyRepeats(38));
378     try std.testing.expect(!state.keyRepeats(39));
379 }
380 
381 test "effective layout and per-key group rules are distinct" {
382     var storage: keymap.Keymap.Storage = undefined;
383     var state = State.init(try keymap.Keymap.init(&storage, Fixture.definition()));
384     const changed = state.update(.{ .depressed_layout = 3 });
385     try std.testing.expect(changed.depressed_layout);
386     try std.testing.expect(changed.effective_layout);
387     try std.testing.expectEqual(@as(usize, 1), state.effectiveLayout());
388     try std.testing.expectEqual(@as(?usize, 0), state.layout(38));
389     try std.testing.expectEqual(@as(?usize, 1), state.layout(40));
390 }
391 
392 test "empty keymaps retain canonical core modifiers" {
393     var storage: keymap.Keymap.Storage = undefined;
394     const empty = try keymap.Keymap.init(&storage, .{
395         .modifiers = &keymap.core_modifiers,
396         .types = &.{},
397         .keys = &.{},
398     });
399     var state = State.init(empty);
400     _ = state.update(.{
401         .depressed_modifiers = std.math.maxInt(u32),
402         .depressed_layout = std.math.maxInt(u32),
403     });
404     try std.testing.expectEqual(keymap.core_modifier_mask, state.effectiveModifiers());
405     try std.testing.expectEqual(@as(usize, 0), state.effectiveLayout());
406     try std.testing.expect(state.oneSymbol(38) == null);
407 }
408 
409 test "repeat-only keys expose no layout level or symbols" {
410     const keys = [_]keymap.Key{.{ .code = 38, .groups = &.{}, .repeats = true }};
411     var storage: keymap.Keymap.Storage = undefined;
412     const compiled = try keymap.Keymap.init(&storage, .{
413         .modifiers = &keymap.core_modifiers,
414         .types = &.{},
415         .keys = &keys,
416     });
417     const state = State.init(compiled);
418     try std.testing.expect(state.keyRepeats(38));
419     try std.testing.expect(state.layout(38) == null);
420     try std.testing.expect(state.level(38) == null);
421     try std.testing.expectEqual(@as(usize, 0), state.symbols(38).len);
422 }
423 
424 test "symbols exposes the canonical transformed sequence" {
425     var storage: keymap.Keymap.Storage = undefined;
426     var state = State.init(try keymap.Keymap.init(&storage, Fixture.definition()));
427     _ = state.update(.{ .locked_modifiers = 1 << 1 });
428     try std.testing.expectEqualSlices(keysym.Keysym, &.{ @fromBackingInt(@intCast('X')), @fromBackingInt(@intCast('Y')) }, state.symbols(39));
429 }