winapi_easy/ui/
messaging.rs

1//! Window and thread message handling.
2
3use std::{
4    io,
5    ptr,
6};
7
8use windows::Win32::Foundation::{
9    HWND,
10    LPARAM,
11    LRESULT,
12    WPARAM,
13};
14use windows::Win32::UI::Shell::NIN_SELECT;
15use windows::Win32::UI::WindowsAndMessaging::{
16    DefWindowProcW,
17    GetMessagePos,
18    HMENU,
19    PostMessageW,
20    SIZE_MINIMIZED,
21    WM_APP,
22    WM_CLOSE,
23    WM_COMMAND,
24    WM_CONTEXTMENU,
25    WM_DESTROY,
26    WM_MENUCOMMAND,
27    WM_SIZE,
28    WM_TIMER,
29};
30
31use crate::internal::windows_missing::{
32    GET_X_LPARAM,
33    GET_Y_LPARAM,
34    HIWORD,
35    LOWORD,
36    NIN_KEYSELECT,
37};
38use crate::internal::{
39    ResultExt,
40    catch_unwind_and_abort,
41};
42use crate::ui::menu::MenuHandle;
43use crate::ui::{
44    Point,
45    WindowHandle,
46};
47
48#[derive(Clone, PartialEq, Debug)]
49pub struct ListenerMessage {
50    pub window_handle: WindowHandle,
51    pub variant: ListenerMessageVariant,
52}
53
54impl ListenerMessage {
55    fn from_known_raw_message(
56        raw_message: RawMessage,
57        window_handle: WindowHandle,
58    ) -> Option<Self> {
59        let variant = match raw_message.message {
60            value if value >= WM_APP && value <= WM_APP + (u32::from(u8::MAX)) => {
61                ListenerMessageVariant::CustomUserMessage(CustomUserMessage {
62                    message_id: (raw_message.message - WM_APP)
63                        .try_into()
64                        .expect("Message ID should be in u8 range"),
65                    w_param: raw_message.w_param.0,
66                    l_param: raw_message.l_param.0,
67                })
68                .into()
69            }
70            RawMessage::ID_NOTIFICATION_ICON_MSG => {
71                let icon_id = HIWORD(
72                    u32::try_from(raw_message.l_param.0).expect("Icon ID conversion failed"),
73                );
74                let event_code: u32 = LOWORD(
75                    u32::try_from(raw_message.l_param.0).expect("Event code conversion failed"),
76                )
77                .into();
78                let xy_coords = {
79                    // `w_param` does contain the coordinates of the click event, but they are not adjusted for DPI scaling, so we can't use them.
80                    // Instead we have to call `GetMessagePos`, which will however return mouse coordinates even if the keyboard was used.
81                    // An alternative would be to use `NOTIFYICON_VERSION_4`, but that would not allow exposing an API for rich pop-up UIs
82                    // when the user hovers over the tray icon since the necessary notifications would not be sent.
83                    // See also: https://stackoverflow.com/a/41649787
84                    let raw_position = unsafe { GetMessagePos() };
85                    get_param_xy_coords(raw_position)
86                };
87                match event_code {
88                    // NIN_SELECT only happens with left clicks. Space will produce 1x NIN_KEYSELECT, Enter 2x NIN_KEYSELECT.
89                    NIN_SELECT | NIN_KEYSELECT => {
90                        ListenerMessageVariant::NotificationIconSelect { icon_id, xy_coords }.into()
91                    }
92                    // Works both with mouse right click and the context menu key.
93                    WM_CONTEXTMENU => {
94                        ListenerMessageVariant::NotificationIconContextSelect { icon_id, xy_coords }
95                            .into()
96                    }
97                    _ => None,
98                }
99            }
100            WM_COMMAND if HIWORD(u32::try_from(raw_message.w_param.0).unwrap()) == 0 => {
101                // Not preferable since unly u16 IDs are supported
102                ListenerMessageVariant::MenuCommand {
103                    selected_item_id: u32::from(LOWORD(
104                        u32::try_from(raw_message.w_param.0).unwrap(),
105                    )),
106                }
107                .into()
108            }
109            WM_MENUCOMMAND => {
110                let menu_handle = MenuHandle::from_maybe_null(HMENU(
111                    ptr::with_exposed_provenance_mut(raw_message.l_param.0.cast_unsigned()),
112                ))
113                .expect("Menu handle should not be null here");
114                let selected_item_id = menu_handle
115                    .get_item_id(raw_message.w_param.0.try_into().unwrap())
116                    .unwrap();
117                ListenerMessageVariant::MenuCommand { selected_item_id }.into()
118            }
119            WM_SIZE => {
120                if raw_message.w_param.0 == SIZE_MINIMIZED.try_into().unwrap() {
121                    ListenerMessageVariant::WindowMinimized.into()
122                } else {
123                    None
124                }
125            }
126            WM_TIMER => ListenerMessageVariant::Timer {
127                timer_id: raw_message.w_param.0,
128            }
129            .into(),
130            WM_CLOSE => ListenerMessageVariant::WindowClose.into(),
131            WM_DESTROY => {
132                // Preempt Windows from destroying the window's menu and submenus automatically
133                window_handle
134                    .set_menu(None)
135                    .unwrap_or_default_and_print_error();
136                ListenerMessageVariant::WindowDestroy.into()
137            }
138            _ => None,
139        };
140        variant.map(|variant| ListenerMessage {
141            window_handle,
142            variant,
143        })
144    }
145}
146
147#[derive(Clone, PartialEq, Debug)]
148pub enum ListenerMessageVariant {
149    MenuCommand {
150        selected_item_id: u32,
151    },
152    WindowMinimized,
153    WindowClose,
154    WindowDestroy,
155    NotificationIconSelect {
156        icon_id: u16,
157        xy_coords: Point,
158    },
159    NotificationIconContextSelect {
160        icon_id: u16,
161        xy_coords: Point,
162    },
163    Timer {
164        timer_id: usize,
165    },
166    /// Message generated from raw message ID values between `WM_APP` and `WM_APP + u8::MAX` exclusive.
167    ///
168    /// Message ID `0` represents the raw value `WM_APP`.
169    CustomUserMessage(CustomUserMessage),
170}
171
172/// Indicates what should be done after the user listener is done processing the message.
173#[derive(Copy, Clone, Default, Debug)]
174pub enum ListenerAnswer {
175    /// Call the default windows handler after the current message processing code.
176    #[default]
177    CallDefaultHandler,
178    /// Message processing is finished, skip calling the windows handler.
179    StopMessageProcessing,
180}
181
182impl ListenerAnswer {
183    fn to_raw_lresult(self) -> Option<LRESULT> {
184        match self {
185            ListenerAnswer::CallDefaultHandler => None,
186            ListenerAnswer::StopMessageProcessing => Some(LRESULT(0)),
187        }
188    }
189}
190
191pub(crate) type WmlOpaqueClosure<'a> = Box<dyn FnMut(&ListenerMessage) -> ListenerAnswer + 'a>;
192
193#[derive(Copy, Clone, Debug)]
194pub(crate) struct RawMessage {
195    pub(crate) message: u32,
196    pub(crate) w_param: WPARAM,
197    pub(crate) l_param: LPARAM,
198}
199
200impl RawMessage {
201    /// Start of the message range for string message registered by `RegisterWindowMessage`.
202    ///
203    /// Values between `WM_APP` and this value (exclusive) can be used for private message IDs
204    /// that won't conflict with messages from predefined Windows control classes.
205    const STR_MSG_RANGE_START: u32 = 0xC000;
206
207    pub(crate) const ID_WINDOW_PROC_MSG: u32 = Self::STR_MSG_RANGE_START - 1;
208    pub(crate) const ID_APP_WAKEUP_MSG: u32 = Self::STR_MSG_RANGE_START - 2;
209    pub(crate) const ID_NOTIFICATION_ICON_MSG: u32 = Self::STR_MSG_RANGE_START - 3;
210
211    /// Posts a message to the thread message queue and returns immediately.
212    ///
213    /// If no window is given, the window procedure won't be called by `DispatchMessageW`.
214    pub(crate) fn post_to_queue(&self, window: Option<WindowHandle>) -> io::Result<()> {
215        unsafe {
216            PostMessageW(
217                window.map(Into::into),
218                self.message,
219                self.w_param,
220                self.l_param,
221            )?;
222        }
223        Ok(())
224    }
225
226    fn post_window_proc_message(listener_message: ListenerMessage) -> io::Result<()> {
227        let ptr_usize = Box::into_raw(Box::new(listener_message)).expose_provenance();
228        let window_proc_message = RawMessage {
229            message: Self::ID_WINDOW_PROC_MSG,
230            w_param: WPARAM(ptr_usize),
231            l_param: LPARAM(0),
232        };
233        window_proc_message.post_to_queue(None)
234    }
235
236    #[expect(dead_code)]
237    fn post_loop_wakeup_message() -> io::Result<()> {
238        let wakeup_message = RawMessage {
239            message: Self::ID_APP_WAKEUP_MSG,
240            w_param: WPARAM(0),
241            l_param: LPARAM(0),
242        };
243        wakeup_message.post_to_queue(None)
244    }
245}
246
247impl From<CustomUserMessage> for RawMessage {
248    fn from(custom_message: CustomUserMessage) -> Self {
249        RawMessage {
250            message: WM_APP + u32::from(custom_message.message_id),
251            w_param: WPARAM(custom_message.w_param),
252            l_param: LPARAM(custom_message.l_param),
253        }
254    }
255}
256
257#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
258pub struct CustomUserMessage {
259    pub message_id: u8,
260    pub w_param: usize,
261    pub l_param: isize,
262}
263
264pub(crate) unsafe extern "system" fn generic_window_proc(
265    h_wnd: HWND,
266    message: u32,
267    w_param: WPARAM,
268    l_param: LPARAM,
269) -> LRESULT {
270    let call = move || {
271        let window = WindowHandle::from_maybe_null(h_wnd)
272            .expect("Window handle given to window procedure should never be NULL");
273
274        let raw_message = RawMessage {
275            message,
276            w_param,
277            l_param,
278        };
279
280        let listener_message = ListenerMessage::from_known_raw_message(raw_message, window);
281        // When creating a window, the custom data for the loop is not set yet
282        // before the first call to this function
283        let listener_result = unsafe { window.get_user_data_ptr::<WmlOpaqueClosure>() }.and_then(
284            |mut listener_ptr| {
285                if let Some(known_listener_message) = &listener_message {
286                    (unsafe { listener_ptr.as_mut().as_mut() })(known_listener_message)
287                        .to_raw_lresult()
288                } else {
289                    ListenerAnswer::default().to_raw_lresult()
290                }
291            },
292        );
293        if let Some(known_listener_message) = listener_message {
294            // Many messages won't go through the thread message loop at all, so we need to notify it.
295            // No chance of an infinite loop here since the window procedure won't be called for messages with no associated windows.
296            // Also note that the window procedure may be called multiple times while the thread message loop is blocked (waiting).
297            RawMessage::post_window_proc_message(known_listener_message)
298                .expect("Cannot send internal window procedure message");
299        }
300
301        if let Some(l_result) = listener_result {
302            l_result
303        } else {
304            unsafe { DefWindowProcW(h_wnd, message, w_param, l_param) }
305        }
306    };
307    catch_unwind_and_abort(call)
308}
309
310fn get_param_xy_coords(param: u32) -> Point {
311    let param = LPARAM(param.try_into().unwrap());
312    Point {
313        x: GET_X_LPARAM(param),
314        y: GET_Y_LPARAM(param),
315    }
316}