winapi_easy/ui/
messaging.rs

1//! Window and thread message handling.
2
3use std::io;
4
5use windows::Win32::Foundation::{
6    HWND,
7    LPARAM,
8    LRESULT,
9    WPARAM,
10};
11use windows::Win32::UI::Shell::NIN_SELECT;
12use windows::Win32::UI::WindowsAndMessaging::{
13    DefWindowProcW,
14    GetMessagePos,
15    PostMessageW,
16    HMENU,
17    SIZE_MINIMIZED,
18    WM_APP,
19    WM_CLOSE,
20    WM_CONTEXTMENU,
21    WM_DESTROY,
22    WM_MENUCOMMAND,
23    WM_SIZE,
24};
25
26use crate::internal::catch_unwind_and_abort;
27use crate::internal::windows_missing::*;
28use crate::messaging::ThreadMessageLoop;
29use crate::ui::menu::MenuHandle;
30use crate::ui::{
31    Point,
32    WindowHandle,
33};
34
35/// Indicates what should be done after the [`WindowMessageListener`] is done processing the message.
36#[derive(Copy, Clone, Default, Debug)]
37pub enum ListenerAnswer {
38    /// Call the default windows handler after the current message processing code.
39    #[default]
40    CallDefaultHandler,
41    /// Message processing is finished, skip calling the windows handler.
42    MessageProcessed,
43}
44
45impl ListenerAnswer {
46    fn to_raw_lresult(self) -> Option<LRESULT> {
47        match self {
48            ListenerAnswer::CallDefaultHandler => None,
49            ListenerAnswer::MessageProcessed => Some(LRESULT(0)),
50        }
51    }
52}
53
54/// A user-defined implementation for various windows message handlers.
55///
56/// The trait already defines a default for all methods, making it easier to just implement specific ones.
57///
58/// # Design rationale
59///
60/// The way the Windows API is structured, it doesn't seem to be possible to use closures here
61/// due to [`crate::ui::Window`] and [`crate::ui::WindowClass`] needing type parameters for the [`WindowMessageListener`],
62/// making it hard to swap out the listener since every `Fn` has its own type in Rust.
63///
64/// `Box` with dynamic dispatch `Fn` is also not practical due to allowing only `'static` lifetimes.
65pub trait WindowMessageListener {
66    /// An item from a window's menu was selected by the user.
67    #[allow(unused_variables)]
68    #[inline(always)]
69    fn handle_menu_command(&self, window: &WindowHandle, selected_item_id: u32) {}
70    /// A 'minimize window' action was performed.
71    #[allow(unused_variables)]
72    #[inline(always)]
73    fn handle_window_minimized(&self, window: &WindowHandle) {}
74    /// A 'close window' action was performed.
75    #[allow(unused_variables)]
76    #[inline(always)]
77    fn handle_window_close(&self, window: &WindowHandle) -> ListenerAnswer {
78        Default::default()
79    }
80    /// A window was destroyed and removed from the screen.
81    #[allow(unused_variables)]
82    #[inline(always)]
83    fn handle_window_destroy(&self, window: &WindowHandle) {}
84    /// A notification icon was selected (triggered).
85    #[allow(unused_variables)]
86    #[inline(always)]
87    fn handle_notification_icon_select(&self, icon_id: u16, xy_coords: Point) {}
88    /// A notification icon was context-selected (e.g. right-clicked).
89    #[allow(unused_variables)]
90    #[inline(always)]
91    fn handle_notification_icon_context_select(&self, icon_id: u16, xy_coords: Point) {}
92    /// A custom user message was sent.
93    #[allow(unused_variables)]
94    #[inline(always)]
95    fn handle_custom_user_message(
96        &self,
97        window: &WindowHandle,
98        message_id: u8,
99        w_param: WPARAM,
100        l_param: LPARAM,
101    ) {
102    }
103}
104
105/// A [`WindowMessageListener`] that leaves all handlers to their default empty impls.
106#[derive(Copy, Clone, Default, Debug)]
107pub struct EmptyWindowMessageListener;
108
109impl WindowMessageListener for EmptyWindowMessageListener {}
110
111#[derive(Copy, Clone, Debug)]
112pub(crate) struct RawMessage {
113    pub(crate) message: u32,
114    pub(crate) w_param: WPARAM,
115    pub(crate) l_param: LPARAM,
116}
117
118impl RawMessage {
119    /// Start of the message range for string message registered by `RegisterWindowMessage`.
120    ///
121    /// Values between `WM_APP` and this value (exclusive) can be used for private message IDs
122    /// that won't conflict with messages from predefined Windows control classes.
123    const STR_MSG_RANGE_START: u32 = 0xC000;
124
125    pub(crate) const ID_APP_WAKEUP_MSG: u32 = Self::STR_MSG_RANGE_START - 1;
126    pub(crate) const ID_NOTIFICATION_ICON_MSG: u32 = Self::STR_MSG_RANGE_START - 2;
127
128    pub(crate) fn dispatch_to_message_listener<WML: WindowMessageListener>(
129        self,
130        window: WindowHandle,
131        listener: &WML,
132    ) -> Option<LRESULT> {
133        // Many messages won't go through the thread message loop, so we need to notify it.
134        // No chance of an infinite loop here since the window procedure won't be called for messages with no associated windows.
135        Self::post_loop_wakeup_message().unwrap();
136        let mut call_message_loop_callback = true;
137        let result = match self.message {
138            value if value >= WM_APP && value <= WM_APP + (u32::from(u8::MAX)) => {
139                listener.handle_custom_user_message(
140                    &window,
141                    (self.message - WM_APP).try_into().unwrap(),
142                    self.w_param,
143                    self.l_param,
144                );
145                None
146            }
147            Self::ID_NOTIFICATION_ICON_MSG => {
148                let icon_id =
149                    HIWORD(u32::try_from(self.l_param.0).expect("Icon ID conversion failed"));
150                let event_code: u32 =
151                    LOWORD(u32::try_from(self.l_param.0).expect("Event code conversion failed"))
152                        .into();
153                let xy_coords = {
154                    // `w_param` does contain the coordinates of the click event, but they are not adjusted for DPI scaling, so we can't use them.
155                    // Instead we have to call `GetMessagePos`, which will however return mouse coordinates even if the keyboard was used.
156                    // An alternative would be to use `NOTIFYICON_VERSION_4`, but that would not allow exposing an API for rich pop-up UIs
157                    // when the user hovers over the tray icon since the necessary notifications would not be sent.
158                    // See also: https://stackoverflow.com/a/41649787
159                    let raw_position = unsafe { GetMessagePos() };
160                    get_param_xy_coords(raw_position)
161                };
162                match event_code {
163                    // NIN_SELECT only happens with left clicks. Space will produce 1x NIN_KEYSELECT, Enter 2x NIN_KEYSELECT.
164                    NIN_SELECT | NIN_KEYSELECT => {
165                        listener.handle_notification_icon_select(icon_id, xy_coords)
166                    }
167                    // Works both with mouse right click and the context menu key.
168                    WM_CONTEXTMENU => {
169                        listener.handle_notification_icon_context_select(icon_id, xy_coords)
170                    }
171                    _ => (),
172                }
173                None
174            }
175            WM_MENUCOMMAND => {
176                let menu_handle =
177                    MenuHandle::from_maybe_null(HMENU(self.l_param.0 as *mut std::ffi::c_void))
178                        .expect("Menu handle should not be null here");
179                let item_id = menu_handle
180                    .get_item_id(self.w_param.0.try_into().unwrap())
181                    .unwrap();
182                listener.handle_menu_command(&window, item_id);
183                None
184            }
185            WM_SIZE => {
186                if self.w_param.0 == SIZE_MINIMIZED.try_into().unwrap() {
187                    listener.handle_window_minimized(&window);
188                }
189                None
190            }
191            WM_CLOSE => listener.handle_window_close(&window).to_raw_lresult(),
192            WM_DESTROY => {
193                listener.handle_window_destroy(&window);
194                None
195            }
196            _ => {
197                call_message_loop_callback = false;
198                None
199            }
200        };
201        if call_message_loop_callback {
202            ThreadMessageLoop::ENABLE_CALLBACK_ONCE.with(|x| x.set(true));
203        }
204        result
205    }
206
207    /// Posts a message to the thread message queue and returns immediately.
208    ///
209    /// If no window is given, the window procedure won't be called by `DispatchMessageW`.
210    fn post_to_queue(&self, window: Option<&WindowHandle>) -> io::Result<()> {
211        unsafe {
212            PostMessageW(
213                window.map(|x| &x.raw_handle),
214                self.message,
215                self.w_param,
216                self.l_param,
217            )?;
218        }
219        Ok(())
220    }
221
222    fn post_loop_wakeup_message() -> io::Result<()> {
223        let wakeup_message = RawMessage {
224            message: Self::ID_APP_WAKEUP_MSG,
225            w_param: WPARAM(0),
226            l_param: LPARAM(0),
227        };
228        wakeup_message.post_to_queue(None)
229    }
230}
231
232pub(crate) unsafe extern "system" fn generic_window_proc<WML>(
233    h_wnd: HWND,
234    message: u32,
235    w_param: WPARAM,
236    l_param: LPARAM,
237) -> LRESULT
238where
239    WML: WindowMessageListener,
240{
241    let call = move || {
242        let window = WindowHandle::from_maybe_null(h_wnd)
243            .expect("Window handle given to window procedure should never be NULL");
244
245        let raw_message = RawMessage {
246            message,
247            w_param,
248            l_param,
249        };
250
251        // When creating a window, the custom data for the loop is not set yet
252        // before the first call to this function
253        let listener_result = window.get_user_data_ptr::<WML>().and_then(|listener_ptr| {
254            raw_message.dispatch_to_message_listener(window, listener_ptr.as_ref())
255        });
256
257        if let Some(l_result) = listener_result {
258            l_result
259        } else {
260            DefWindowProcW(h_wnd, message, w_param, l_param)
261        }
262    };
263    catch_unwind_and_abort(call)
264}
265
266fn get_param_xy_coords(param: u32) -> Point {
267    let param = LPARAM(param.try_into().unwrap());
268    Point {
269        x: GET_X_LPARAM(param),
270        y: GET_Y_LPARAM(param),
271    }
272}