| 1 | const std = @import("std"); |
| 2 | const utils = @import("utils.zig"); |
| 3 | const res = @import("res.zig"); |
| 4 | const SourceBytes = @import("literals.zig").SourceBytes; |
| 5 | |
| 6 | // https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files |
| 7 | |
| 8 | pub 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(@fromBackingInt(@intCast(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)", .{@backingInt(res.RT.CURSOR)}), |
| 137 | .icon_num => std.fmt.comptimePrint("{d} (icon)", .{@backingInt(res.RT.ICON)}), |
| 138 | .string_num => std.fmt.comptimePrint("{d} (string)", .{@backingInt(res.RT.STRING)}), |
| 139 | .anicursor_num => std.fmt.comptimePrint("{d} (anicursor)", .{@backingInt(res.RT.ANICURSOR)}), |
| 140 | .aniicon_num => std.fmt.comptimePrint("{d} (aniicon)", .{@backingInt(res.RT.ANIICON)}), |
| 141 | .fontdir_num => std.fmt.comptimePrint("{d} (fontdir)", .{@backingInt(res.RT.FONTDIR)}), |
| 142 | .manifest_num => std.fmt.comptimePrint("{d} (manifest)", .{@backingInt(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 |
| 150 | pub 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 | |
| 185 | pub 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 | |
| 245 | pub 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 | |
| 279 | const 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 | |
| 294 | pub 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 | |
| 332 | pub 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 | |
| 345 | pub 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 | |
| 368 | pub 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. |
| 383 | pub 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 | |
| 400 | pub 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 | |
| 427 | pub 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 | }; |