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 }