winapi_easy/ui/
resource.rs

1//! Application resources (icons, cursors and brushes).
2
3use 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        /// Destroys a non-shared image handle.
95        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    /// Standard arrow
338    #[default]
339    Normal = OCR_NORMAL.0,
340    /// Standard arrow and small hourglass
341    NormalPlusWaiting = OCR_APPSTARTING.0,
342    /// Hourglass
343    Waiting = OCR_WAIT.0,
344    /// Arrow and question mark
345    NormalPlusQuestionMark = OCR_HELP.0,
346    /// Crosshair
347    Crosshair = OCR_CROSS.0,
348    /// Hand
349    Hand = OCR_HAND.0,
350    /// I-beam
351    IBeam = OCR_IBEAM.0,
352    /// Slashed circle
353    SlashedCircle = OCR_NO.0,
354    /// Four-pointed arrow pointing north, south, east, and west
355    ArrowAllDirections = OCR_SIZEALL.0,
356    /// Double-pointed arrow pointing northeast and southwest
357    ArrowNESW = OCR_SIZENESW.0,
358    /// Double-pointed arrow pointing north and south
359    ArrowNS = OCR_SIZENS.0,
360    /// Double-pointed arrow pointing northwest and southeast
361    ArrowNWSE = OCR_SIZENWSE.0,
362    /// Double-pointed arrow pointing west and east
363    ArrowWE = OCR_SIZEWE.0,
364    /// Vertical arrow
365    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    // Temporary function until this gets resolved: https://github.com/microsoft/windows-rs/issues/641
498    #[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}