master
1const std = @import("std");
2const utils = @import("utils.zig");
3const res = @import("res.zig");
4const SourceBytes = @import("literals.zig").SourceBytes;
5
6// https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files
7
8pub const ResourceType = enum {
9 accelerators,
10 bitmap,
11 cursor,
12 dialog,
13 dialogex,
14 /// As far as I can tell, this is undocumented; the most I could find was this:
15 /// https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/91697
16 dlginclude,
17 /// Undocumented, basically works exactly like RCDATA
18 dlginit,
19 font,
20 html,
21 icon,
22 menu,
23 menuex,
24 messagetable,
25 plugplay, // Obsolete
26 rcdata,
27 stringtable,
28 /// Undocumented
29 toolbar,
30 user_defined,
31 versioninfo,
32 vxd, // Obsolete
33
34 // Types that are treated as a user-defined type when encountered, but have
35 // special meaning without the Visual Studio GUI. We match the Win32 RC compiler
36 // behavior by acting as if these keyword don't exist when compiling the .rc
37 // (thereby treating them as user-defined).
38 //textinclude, // A special resource that is interpreted by Visual C++.
39 //typelib, // A special resource that is used with the /TLBID and /TLBOUT linker options
40
41 // Types that can only be specified by numbers, they don't have keywords
42 cursor_num,
43 icon_num,
44 string_num,
45 anicursor_num,
46 aniicon_num,
47 fontdir_num,
48 manifest_num,
49
50 const map = std.StaticStringMapWithEql(
51 ResourceType,
52 std.static_string_map.eqlAsciiIgnoreCase,
53 ).initComptime(.{
54 .{ "ACCELERATORS", .accelerators },
55 .{ "BITMAP", .bitmap },
56 .{ "CURSOR", .cursor },
57 .{ "DIALOG", .dialog },
58 .{ "DIALOGEX", .dialogex },
59 .{ "DLGINCLUDE", .dlginclude },
60 .{ "DLGINIT", .dlginit },
61 .{ "FONT", .font },
62 .{ "HTML", .html },
63 .{ "ICON", .icon },
64 .{ "MENU", .menu },
65 .{ "MENUEX", .menuex },
66 .{ "MESSAGETABLE", .messagetable },
67 .{ "PLUGPLAY", .plugplay },
68 .{ "RCDATA", .rcdata },
69 .{ "STRINGTABLE", .stringtable },
70 .{ "TOOLBAR", .toolbar },
71 .{ "VERSIONINFO", .versioninfo },
72 .{ "VXD", .vxd },
73 });
74
75 pub fn fromString(bytes: SourceBytes) ResourceType {
76 const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(bytes);
77 if (maybe_ordinal) |ordinal| {
78 if (ordinal.ordinal >= 256) return .user_defined;
79 return fromRT(@enumFromInt(ordinal.ordinal));
80 }
81 return map.get(bytes.slice) orelse .user_defined;
82 }
83
84 // TODO: Some comptime validation that RT <-> ResourceType conversion is synced?
85 pub fn fromRT(rt: res.RT) ResourceType {
86 return switch (rt) {
87 .ACCELERATOR => .accelerators,
88 .ANICURSOR => .anicursor_num,
89 .ANIICON => .aniicon_num,
90 .BITMAP => .bitmap,
91 .CURSOR => .cursor_num,
92 .DIALOG => .dialog,
93 .DLGINCLUDE => .dlginclude,
94 .DLGINIT => .dlginit,
95 .FONT => .font,
96 .FONTDIR => .fontdir_num,
97 .GROUP_CURSOR => .cursor,
98 .GROUP_ICON => .icon,
99 .HTML => .html,
100 .ICON => .icon_num,
101 .MANIFEST => .manifest_num,
102 .MENU => .menu,
103 .MESSAGETABLE => .messagetable,
104 .PLUGPLAY => .plugplay,
105 .RCDATA => .rcdata,
106 .STRING => .string_num,
107 .TOOLBAR => .toolbar,
108 .VERSION => .versioninfo,
109 .VXD => .vxd,
110 _ => .user_defined,
111 };
112 }
113
114 pub fn canUseRawData(resource: ResourceType) bool {
115 return switch (resource) {
116 .user_defined,
117 .html,
118 .plugplay, // Obsolete
119 .rcdata,
120 .vxd, // Obsolete
121 .manifest_num,
122 .dlginit,
123 => true,
124 else => false,
125 };
126 }
127
128 pub fn nameForErrorDisplay(resource: ResourceType) []const u8 {
129 return switch (resource) {
130 // zig fmt: off
131 .accelerators, .bitmap, .cursor, .dialog, .dialogex, .dlginclude, .dlginit, .font,
132 .html, .icon, .menu, .menuex, .messagetable, .plugplay, .rcdata, .stringtable,
133 .toolbar, .versioninfo, .vxd => @tagName(resource),
134 // zig fmt: on
135 .user_defined => "user-defined",
136 .cursor_num => std.fmt.comptimePrint("{d} (cursor)", .{@intFromEnum(res.RT.CURSOR)}),
137 .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@intFromEnum(res.RT.ICON)}),
138 .string_num => std.fmt.comptimePrint("{d} (string)", .{@intFromEnum(res.RT.STRING)}),
139 .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@intFromEnum(res.RT.ANICURSOR)}),
140 .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@intFromEnum(res.RT.ANIICON)}),
141 .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@intFromEnum(res.RT.FONTDIR)}),
142 .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@intFromEnum(res.RT.MANIFEST)}),
143 };
144 }
145};
146
147/// https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable-resource#parameters
148/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialog-resource#parameters
149/// https://learn.microsoft.com/en-us/windows/win32/menurc/dialogex-resource#parameters
150pub const OptionalStatements = enum {
151 characteristics,
152 language,
153 version,
154
155 // DIALOG
156 caption,
157 class,
158 exstyle,
159 font,
160 menu,
161 style,
162
163 pub const map = std.StaticStringMapWithEql(
164 OptionalStatements,
165 std.static_string_map.eqlAsciiIgnoreCase,
166 ).initComptime(.{
167 .{ "CHARACTERISTICS", .characteristics },
168 .{ "LANGUAGE", .language },
169 .{ "VERSION", .version },
170 });
171
172 pub const dialog_map = std.StaticStringMapWithEql(
173 OptionalStatements,
174 std.static_string_map.eqlAsciiIgnoreCase,
175 ).initComptime(.{
176 .{ "CAPTION", .caption },
177 .{ "CLASS", .class },
178 .{ "EXSTYLE", .exstyle },
179 .{ "FONT", .font },
180 .{ "MENU", .menu },
181 .{ "STYLE", .style },
182 });
183};
184
185pub const Control = enum {
186 auto3state,
187 autocheckbox,
188 autoradiobutton,
189 checkbox,
190 combobox,
191 control,
192 ctext,
193 defpushbutton,
194 edittext,
195 hedit,
196 iedit,
197 groupbox,
198 icon,
199 listbox,
200 ltext,
201 pushbox,
202 pushbutton,
203 radiobutton,
204 rtext,
205 scrollbar,
206 state3,
207 userbutton,
208
209 pub const map = std.StaticStringMapWithEql(
210 Control,
211 std.static_string_map.eqlAsciiIgnoreCase,
212 ).initComptime(.{
213 .{ "AUTO3STATE", .auto3state },
214 .{ "AUTOCHECKBOX", .autocheckbox },
215 .{ "AUTORADIOBUTTON", .autoradiobutton },
216 .{ "CHECKBOX", .checkbox },
217 .{ "COMBOBOX", .combobox },
218 .{ "CONTROL", .control },
219 .{ "CTEXT", .ctext },
220 .{ "DEFPUSHBUTTON", .defpushbutton },
221 .{ "EDITTEXT", .edittext },
222 .{ "HEDIT", .hedit },
223 .{ "IEDIT", .iedit },
224 .{ "GROUPBOX", .groupbox },
225 .{ "ICON", .icon },
226 .{ "LISTBOX", .listbox },
227 .{ "LTEXT", .ltext },
228 .{ "PUSHBOX", .pushbox },
229 .{ "PUSHBUTTON", .pushbutton },
230 .{ "RADIOBUTTON", .radiobutton },
231 .{ "RTEXT", .rtext },
232 .{ "SCROLLBAR", .scrollbar },
233 .{ "STATE3", .state3 },
234 .{ "USERBUTTON", .userbutton },
235 });
236
237 pub fn hasTextParam(control: Control) bool {
238 switch (control) {
239 .scrollbar, .listbox, .iedit, .hedit, .edittext, .combobox => return false,
240 else => return true,
241 }
242 }
243};
244
245pub const ControlClass = struct {
246 pub const map = std.StaticStringMapWithEql(
247 res.ControlClass,
248 std.static_string_map.eqlAsciiIgnoreCase,
249 ).initComptime(.{
250 .{ "BUTTON", .button },
251 .{ "EDIT", .edit },
252 .{ "STATIC", .static },
253 .{ "LISTBOX", .listbox },
254 .{ "SCROLLBAR", .scrollbar },
255 .{ "COMBOBOX", .combobox },
256 });
257
258 /// Like `map.get` but works on WTF16 strings, for use with parsed
259 /// string literals ("BUTTON", or even "\x42UTTON")
260 pub fn fromWideString(str: []const u16) ?res.ControlClass {
261 const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
262 return if (ascii.eqlIgnoreCaseW(str, utf16Literal("BUTTON")))
263 .button
264 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("EDIT")))
265 .edit
266 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("STATIC")))
267 .static
268 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("LISTBOX")))
269 .listbox
270 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("SCROLLBAR")))
271 .scrollbar
272 else if (ascii.eqlIgnoreCaseW(str, utf16Literal("COMBOBOX")))
273 .combobox
274 else
275 null;
276 }
277};
278
279const ascii = struct {
280 /// Compares ASCII values case-insensitively, non-ASCII values are compared directly
281 pub fn eqlIgnoreCaseW(a: []const u16, b: []const u16) bool {
282 if (a.len != b.len) return false;
283 for (a, b) |a_c, b_c| {
284 if (a_c < 128) {
285 if (std.ascii.toLower(@intCast(a_c)) != std.ascii.toLower(@intCast(b_c))) return false;
286 } else {
287 if (a_c != b_c) return false;
288 }
289 }
290 return true;
291 }
292};
293
294pub const MenuItem = enum {
295 menuitem,
296 popup,
297
298 pub const map = std.StaticStringMapWithEql(
299 MenuItem,
300 std.static_string_map.eqlAsciiIgnoreCase,
301 ).initComptime(.{
302 .{ "MENUITEM", .menuitem },
303 .{ "POPUP", .popup },
304 });
305
306 pub fn isSeparator(bytes: []const u8) bool {
307 return std.ascii.eqlIgnoreCase(bytes, "SEPARATOR");
308 }
309
310 pub const Option = enum {
311 checked,
312 grayed,
313 help,
314 inactive,
315 menubarbreak,
316 menubreak,
317
318 pub const map = std.StaticStringMapWithEql(
319 Option,
320 std.static_string_map.eqlAsciiIgnoreCase,
321 ).initComptime(.{
322 .{ "CHECKED", .checked },
323 .{ "GRAYED", .grayed },
324 .{ "HELP", .help },
325 .{ "INACTIVE", .inactive },
326 .{ "MENUBARBREAK", .menubarbreak },
327 .{ "MENUBREAK", .menubreak },
328 });
329 };
330};
331
332pub const ToolbarButton = enum {
333 button,
334 separator,
335
336 pub const map = std.StaticStringMapWithEql(
337 ToolbarButton,
338 std.static_string_map.eqlAsciiIgnoreCase,
339 ).initComptime(.{
340 .{ "BUTTON", .button },
341 .{ "SEPARATOR", .separator },
342 });
343};
344
345pub const VersionInfo = enum {
346 file_version,
347 product_version,
348 file_flags_mask,
349 file_flags,
350 file_os,
351 file_type,
352 file_subtype,
353
354 pub const map = std.StaticStringMapWithEql(
355 VersionInfo,
356 std.static_string_map.eqlAsciiIgnoreCase,
357 ).initComptime(.{
358 .{ "FILEVERSION", .file_version },
359 .{ "PRODUCTVERSION", .product_version },
360 .{ "FILEFLAGSMASK", .file_flags_mask },
361 .{ "FILEFLAGS", .file_flags },
362 .{ "FILEOS", .file_os },
363 .{ "FILETYPE", .file_type },
364 .{ "FILESUBTYPE", .file_subtype },
365 });
366};
367
368pub const VersionBlock = enum {
369 block,
370 value,
371
372 pub const map = std.StaticStringMapWithEql(
373 VersionBlock,
374 std.static_string_map.eqlAsciiIgnoreCase,
375 ).initComptime(.{
376 .{ "BLOCK", .block },
377 .{ "VALUE", .value },
378 });
379};
380
381/// Keywords that are be the first token in a statement and (if so) dictate how the rest
382/// of the statement is parsed.
383pub const TopLevelKeywords = enum {
384 language,
385 version,
386 characteristics,
387 stringtable,
388
389 pub const map = std.StaticStringMapWithEql(
390 TopLevelKeywords,
391 std.static_string_map.eqlAsciiIgnoreCase,
392 ).initComptime(.{
393 .{ "LANGUAGE", .language },
394 .{ "VERSION", .version },
395 .{ "CHARACTERISTICS", .characteristics },
396 .{ "STRINGTABLE", .stringtable },
397 });
398};
399
400pub const CommonResourceAttributes = enum {
401 preload,
402 loadoncall,
403 fixed,
404 moveable,
405 discardable,
406 pure,
407 impure,
408 shared,
409 nonshared,
410
411 pub const map = std.StaticStringMapWithEql(
412 CommonResourceAttributes,
413 std.static_string_map.eqlAsciiIgnoreCase,
414 ).initComptime(.{
415 .{ "PRELOAD", .preload },
416 .{ "LOADONCALL", .loadoncall },
417 .{ "FIXED", .fixed },
418 .{ "MOVEABLE", .moveable },
419 .{ "DISCARDABLE", .discardable },
420 .{ "PURE", .pure },
421 .{ "IMPURE", .impure },
422 .{ "SHARED", .shared },
423 .{ "NONSHARED", .nonshared },
424 });
425};
426
427pub const AcceleratorTypeAndOptions = enum {
428 virtkey,
429 ascii,
430 noinvert,
431 alt,
432 shift,
433 control,
434
435 pub const map = std.StaticStringMapWithEql(
436 AcceleratorTypeAndOptions,
437 std.static_string_map.eqlAsciiIgnoreCase,
438 ).initComptime(.{
439 .{ "VIRTKEY", .virtkey },
440 .{ "ASCII", .ascii },
441 .{ "NOINVERT", .noinvert },
442 .{ "ALT", .alt },
443 .{ "SHIFT", .shift },
444 .{ "CONTROL", .control },
445 });
446};