1use std::path::Path;
4use std::{
5 io,
6 ptr,
7};
8
9use num_enum::IntoPrimitive;
10use windows::Win32::Foundation::HANDLE;
11use windows::Win32::Graphics::Gdi::{
12 COLOR_3DDKSHADOW,
13 COLOR_3DLIGHT,
14 COLOR_ACTIVEBORDER,
15 COLOR_ACTIVECAPTION,
16 COLOR_APPWORKSPACE,
17 COLOR_BACKGROUND,
18 COLOR_BTNFACE,
19 COLOR_BTNHIGHLIGHT,
20 COLOR_BTNSHADOW,
21 COLOR_BTNTEXT,
22 COLOR_CAPTIONTEXT,
23 COLOR_GRADIENTACTIVECAPTION,
24 COLOR_GRADIENTINACTIVECAPTION,
25 COLOR_GRAYTEXT,
26 COLOR_HIGHLIGHT,
27 COLOR_HIGHLIGHTTEXT,
28 COLOR_HOTLIGHT,
29 COLOR_INACTIVEBORDER,
30 COLOR_INACTIVECAPTION,
31 COLOR_INACTIVECAPTIONTEXT,
32 COLOR_INFOBK,
33 COLOR_INFOTEXT,
34 COLOR_MENU,
35 COLOR_MENUBAR,
36 COLOR_MENUHILIGHT,
37 COLOR_MENUTEXT,
38 COLOR_SCROLLBAR,
39 COLOR_WINDOW,
40 COLOR_WINDOWFRAME,
41 COLOR_WINDOWTEXT,
42 HBRUSH,
43};
44use windows::Win32::UI::WindowsAndMessaging::{
45 DestroyCursor,
46 DestroyIcon,
47 GDI_IMAGE_TYPE,
48 HCURSOR,
49 HICON,
50 IMAGE_CURSOR,
51 IMAGE_ICON,
52 LR_DEFAULTSIZE,
53 LR_LOADFROMFILE,
54 LR_SHARED,
55 LoadImageW,
56 OCR_APPSTARTING,
57 OCR_CROSS,
58 OCR_HAND,
59 OCR_HELP,
60 OCR_IBEAM,
61 OCR_NO,
62 OCR_NORMAL,
63 OCR_SIZEALL,
64 OCR_SIZENESW,
65 OCR_SIZENS,
66 OCR_SIZENWSE,
67 OCR_SIZEWE,
68 OCR_UP,
69 OCR_WAIT,
70 OIC_ERROR,
71 OIC_INFORMATION,
72 OIC_QUES,
73 OIC_SAMPLE,
74 OIC_SHIELD,
75 OIC_WARNING,
76};
77use windows_missing::MAKEINTRESOURCEW;
78
79pub(crate) use self::private::*;
80use crate::internal::ResultExt;
81use crate::module::ExecutableModule;
82use crate::string::ZeroTerminatedWideString;
83
84mod private {
85 #[expect(clippy::wildcard_imports)]
86 use super::*;
87
88 pub trait ImageHandleKind: Copy + Sized {
89 type BuiltinType: BuiltinImageKind;
90 const RESOURCE_TYPE: GDI_IMAGE_TYPE;
91
92 fn from_untyped_handle(handle: HANDLE) -> Self;
93
94 fn destroy(self) -> io::Result<()>;
96 }
97
98 pub trait ImageKindInternal {
99 type Handle: ImageHandleKind;
100 fn new_from_loaded_image(loaded_image: LoadedImage<Self::Handle>) -> Self;
101 fn as_handle(&self) -> Self::Handle;
102 }
103
104 pub trait BuiltinImageKind: Into<u32> {
105 fn into_ordinal(self) -> u32 {
106 self.into()
107 }
108 }
109
110 #[derive(Eq, PartialEq, Debug)]
111 pub struct LoadedImage<H: ImageHandleKind> {
112 handle: H,
113 shared: bool,
114 }
115
116 impl<H: ImageHandleKind> LoadedImage<H> {
117 pub(crate) fn from_builtin(builtin: H::BuiltinType) -> io::Result<Self> {
118 Self::load(LoadImageVariant::BuiltinId(builtin.into_ordinal()))
119 }
120
121 pub(crate) fn from_module_by_name(
122 module: &ExecutableModule,
123 name: String,
124 ) -> io::Result<Self> {
125 Self::load(LoadImageVariant::FromModule {
126 module,
127 module_load_variant: LoadImageFromModuleVariant::ByName(name),
128 load_as_shared: true,
129 })
130 }
131
132 pub(crate) fn from_module_by_ordinal(
133 module: &ExecutableModule,
134 ordinal: u32,
135 ) -> io::Result<Self> {
136 Self::load(LoadImageVariant::FromModule {
137 module,
138 module_load_variant: LoadImageFromModuleVariant::ByOrdinal(ordinal),
139 load_as_shared: true,
140 })
141 }
142
143 pub(crate) fn from_file(path: impl AsRef<Path>) -> io::Result<Self> {
144 Self::load(LoadImageVariant::FromFile(path.as_ref()))
145 }
146
147 pub(crate) fn as_handle(&self) -> H {
148 self.handle
149 }
150
151 fn load(load_params: LoadImageVariant) -> io::Result<Self> {
152 let handle_param;
153 let base_flags;
154 let name_data;
155 let name_param;
156 let shared;
157 match load_params {
158 LoadImageVariant::BuiltinId(resource_id) => {
159 handle_param = None;
160 base_flags = Default::default();
161 name_param = MAKEINTRESOURCEW(resource_id);
162 shared = true;
163 }
164 LoadImageVariant::FromModule {
165 module,
166 module_load_variant,
167 load_as_shared,
168 } => {
169 handle_param = Some(module.as_hinstance());
170 base_flags = Default::default();
171 match module_load_variant {
172 LoadImageFromModuleVariant::ByOrdinal(ordinal) => {
173 name_param = MAKEINTRESOURCEW(ordinal);
174 }
175 LoadImageFromModuleVariant::ByName(name) => {
176 name_data = ZeroTerminatedWideString::from_os_str(name);
177 name_param = name_data.as_raw_pcwstr();
178 }
179 }
180 shared = load_as_shared;
181 }
182 LoadImageVariant::FromFile(file_name) => {
183 handle_param = None;
184 base_flags = LR_LOADFROMFILE;
185 name_data = ZeroTerminatedWideString::from_os_str(file_name);
186 name_param = name_data.as_raw_pcwstr();
187 shared = false;
188 }
189 }
190 let flags = if shared {
191 base_flags | LR_SHARED
192 } else {
193 base_flags
194 };
195 let handle = unsafe {
196 LoadImageW(
197 handle_param,
198 name_param,
199 H::RESOURCE_TYPE,
200 0,
201 0,
202 flags | LR_DEFAULTSIZE,
203 )?
204 };
205 let handle = H::from_untyped_handle(handle);
206 Ok(Self { handle, shared })
207 }
208 }
209
210 impl<H: ImageHandleKind> Drop for LoadedImage<H> {
211 fn drop(&mut self) {
212 if !self.shared {
213 self.handle.destroy().unwrap_or_default_and_print_error();
214 }
215 }
216 }
217
218 impl TryFrom<BuiltinIcon> for LoadedImage<HICON> {
219 type Error = io::Error;
220
221 fn try_from(value: BuiltinIcon) -> Result<Self, Self::Error> {
222 Self::from_builtin(value)
223 }
224 }
225
226 impl TryFrom<BuiltinCursor> for LoadedImage<HCURSOR> {
227 type Error = io::Error;
228
229 fn try_from(value: BuiltinCursor) -> Result<Self, Self::Error> {
230 Self::from_builtin(value)
231 }
232 }
233}
234
235impl ImageHandleKind for HICON {
236 type BuiltinType = BuiltinIcon;
237 const RESOURCE_TYPE: GDI_IMAGE_TYPE = IMAGE_ICON;
238
239 fn from_untyped_handle(handle: HANDLE) -> Self {
240 Self(handle.0)
241 }
242
243 fn destroy(self) -> io::Result<()> {
244 unsafe {
245 DestroyIcon(self)?;
246 }
247 Ok(())
248 }
249}
250
251impl ImageHandleKind for HCURSOR {
252 type BuiltinType = BuiltinCursor;
253 const RESOURCE_TYPE: GDI_IMAGE_TYPE = IMAGE_CURSOR;
254
255 fn from_untyped_handle(handle: HANDLE) -> Self {
256 Self(handle.0)
257 }
258
259 fn destroy(self) -> io::Result<()> {
260 unsafe {
261 DestroyCursor(self)?;
262 }
263 Ok(())
264 }
265}
266
267pub trait ImageKind: ImageKindInternal + Sized {
268 fn from_builtin(builtin: <Self::Handle as ImageHandleKind>::BuiltinType) -> Self {
269 Self::new_from_loaded_image(
270 LoadedImage::from_builtin(builtin).unwrap_or_else(|_| unreachable!()),
271 )
272 }
273
274 fn from_module_by_name(module: &ExecutableModule, name: String) -> io::Result<Self> {
275 Ok(Self::new_from_loaded_image(
276 LoadedImage::from_module_by_name(module, name)?,
277 ))
278 }
279
280 fn from_module_by_ordinal(module: &ExecutableModule, ordinal: u32) -> io::Result<Self> {
281 Ok(Self::new_from_loaded_image(
282 LoadedImage::from_module_by_ordinal(module, ordinal)?,
283 ))
284 }
285
286 fn from_file<A: AsRef<Path>>(path: A) -> io::Result<Self> {
287 Ok(Self::new_from_loaded_image(LoadedImage::from_file(path)?))
288 }
289}
290
291#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Default, Debug)]
292#[repr(u32)]
293pub enum BuiltinIcon {
294 #[default]
295 Application = OIC_SAMPLE,
296 QuestionMark = OIC_QUES,
297 Warning = OIC_WARNING,
298 Error = OIC_ERROR,
299 Information = OIC_INFORMATION,
300 Shield = OIC_SHIELD,
301}
302
303impl BuiltinImageKind for BuiltinIcon {}
304
305#[derive(Eq, PartialEq, Debug)]
306pub struct Icon(LoadedImage<HICON>);
307
308impl ImageKindInternal for Icon {
309 type Handle = HICON;
310
311 fn new_from_loaded_image(loaded_image: LoadedImage<Self::Handle>) -> Self {
312 Self(loaded_image)
313 }
314
315 fn as_handle(&self) -> Self::Handle {
316 self.0.as_handle()
317 }
318}
319
320impl ImageKind for Icon {}
321
322impl From<BuiltinIcon> for Icon {
323 fn from(value: BuiltinIcon) -> Self {
324 Self::from_builtin(value)
325 }
326}
327
328impl Default for Icon {
329 fn default() -> Self {
330 Self::from(BuiltinIcon::default())
331 }
332}
333
334#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Default, Debug)]
335#[repr(u32)]
336pub enum BuiltinCursor {
337 #[default]
339 Normal = OCR_NORMAL.0,
340 NormalPlusWaiting = OCR_APPSTARTING.0,
342 Waiting = OCR_WAIT.0,
344 NormalPlusQuestionMark = OCR_HELP.0,
346 Crosshair = OCR_CROSS.0,
348 Hand = OCR_HAND.0,
350 IBeam = OCR_IBEAM.0,
352 SlashedCircle = OCR_NO.0,
354 ArrowAllDirections = OCR_SIZEALL.0,
356 ArrowNESW = OCR_SIZENESW.0,
358 ArrowNS = OCR_SIZENS.0,
360 ArrowNWSE = OCR_SIZENWSE.0,
362 ArrowWE = OCR_SIZEWE.0,
364 Up = OCR_UP.0,
366}
367
368impl BuiltinImageKind for BuiltinCursor {}
369
370#[derive(Eq, PartialEq, Debug)]
371pub struct Cursor(LoadedImage<HCURSOR>);
372
373impl ImageKindInternal for Cursor {
374 type Handle = HCURSOR;
375
376 fn new_from_loaded_image(loaded_image: LoadedImage<Self::Handle>) -> Self {
377 Self(loaded_image)
378 }
379
380 fn as_handle(&self) -> Self::Handle {
381 self.0.as_handle()
382 }
383}
384
385impl ImageKind for Cursor {}
386
387impl From<BuiltinCursor> for Cursor {
388 fn from(value: BuiltinCursor) -> Self {
389 Self::from_builtin(value)
390 }
391}
392
393impl Default for Cursor {
394 fn default() -> Self {
395 Self::from(BuiltinCursor::default())
396 }
397}
398
399#[derive(IntoPrimitive, Copy, Clone, Eq, PartialEq, Default, Debug)]
400#[repr(i32)]
401pub enum BuiltinColor {
402 #[default]
403 Scrollbar = COLOR_SCROLLBAR.0,
404 Background = COLOR_BACKGROUND.0,
405 ActiveCaption = COLOR_ACTIVECAPTION.0,
406 InactiveCaption = COLOR_INACTIVECAPTION.0,
407 Menu = COLOR_MENU.0,
408 Window = COLOR_WINDOW.0,
409 WindowFrame = COLOR_WINDOWFRAME.0,
410 MenuText = COLOR_MENUTEXT.0,
411 WindowText = COLOR_WINDOWTEXT.0,
412 CaptionText = COLOR_CAPTIONTEXT.0,
413 ActiveBorder = COLOR_ACTIVEBORDER.0,
414 InactiveBorder = COLOR_INACTIVEBORDER.0,
415 AppWorkspace = COLOR_APPWORKSPACE.0,
416 Highlight = COLOR_HIGHLIGHT.0,
417 HighlightText = COLOR_HIGHLIGHTTEXT.0,
418 ButtonFace = COLOR_BTNFACE.0,
419 ButtonShadow = COLOR_BTNSHADOW.0,
420 GrayText = COLOR_GRAYTEXT.0,
421 ButtonText = COLOR_BTNTEXT.0,
422 InactiveCaptionText = COLOR_INACTIVECAPTIONTEXT.0,
423 ButtonHighlight = COLOR_BTNHIGHLIGHT.0,
424 Shadow3DDark = COLOR_3DDKSHADOW.0,
425 Light3D = COLOR_3DLIGHT.0,
426 InfoText = COLOR_INFOTEXT.0,
427 InfoBlack = COLOR_INFOBK.0,
428 HotLight = COLOR_HOTLIGHT.0,
429 GradientActiveCaption = COLOR_GRADIENTACTIVECAPTION.0,
430 GradientInactiveCaption = COLOR_GRADIENTINACTIVECAPTION.0,
431 MenuHighlight = COLOR_MENUHILIGHT.0,
432 MenuBar = COLOR_MENUBAR.0,
433}
434
435impl BuiltinColor {
436 fn as_handle(&self) -> HBRUSH {
437 HBRUSH(ptr::with_exposed_provenance_mut(
438 i32::from(*self)
439 .try_into()
440 .unwrap_or_else(|_| unreachable!()),
441 ))
442 }
443}
444
445#[derive(Eq, PartialEq, Debug)]
446enum BrushKind {
447 BuiltinColor(BuiltinColor),
448}
449
450impl BrushKind {
451 pub(crate) fn as_handle(&self) -> HBRUSH {
452 match self {
453 Self::BuiltinColor(builtin_brush) => builtin_brush.as_handle(),
454 }
455 }
456}
457
458impl Default for BrushKind {
459 fn default() -> Self {
460 Self::BuiltinColor(Default::default())
461 }
462}
463
464#[derive(Eq, PartialEq, Default, Debug)]
465pub struct Brush(BrushKind);
466
467impl Brush {
468 pub(crate) fn as_handle(&self) -> HBRUSH {
469 self.0.as_handle()
470 }
471}
472
473impl From<BuiltinColor> for Brush {
474 fn from(value: BuiltinColor) -> Self {
475 Self(BrushKind::BuiltinColor(value))
476 }
477}
478
479enum LoadImageVariant<'a> {
480 BuiltinId(u32),
481 FromModule {
482 module: &'a ExecutableModule,
483 module_load_variant: LoadImageFromModuleVariant,
484 load_as_shared: bool,
485 },
486 FromFile(&'a Path),
487}
488
489enum LoadImageFromModuleVariant {
490 ByOrdinal(u32),
491 ByName(String),
492}
493
494mod windows_missing {
495 use windows::core::PCWSTR;
496
497 #[expect(non_snake_case)]
499 pub fn MAKEINTRESOURCEW(i: u32) -> PCWSTR {
500 PCWSTR(i as usize as *const u16)
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn load_builtin_icon() -> io::Result<()> {
510 let icon = Icon::from_builtin(BuiltinIcon::default());
511 assert!(!icon.as_handle().is_invalid());
512 Ok(())
513 }
514
515 #[test]
516 fn load_shell32_icon() -> io::Result<()> {
517 let module = ExecutableModule::load_module_as_data_file("shell32.dll")?;
518 let icon = Icon::from_module_by_ordinal(&module, 1)?;
519 assert!(!icon.as_handle().is_invalid());
520 Ok(())
521 }
522}