everything_plugin/ui/
mod.rs

1//! ## Everything plugin SDK GUI API
2//! Not implemented, just for comparison:
3//! - Button, Checkbox, Edit, GroupBox, Listbox, NumberEdit, PasswordEdit, Static, Tooltip
4//! - set_text, enable, redraw, Listbox API
5//! - File dialogs
6//! - No dark mode support
7
8use std::{
9    cell::UnsafeCell,
10    ffi::{CString, c_void},
11    fmt::Debug,
12    mem,
13};
14
15use bon::Builder;
16use futures_channel::mpsc;
17use tracing::{debug, trace, warn};
18use windows_sys::Win32::{
19    Foundation::HWND,
20    UI::WindowsAndMessaging::{
21        GA_ROOT, GetAncestor, SWP_NOZORDER, SendMessageW, SetWindowPos, WM_CLOSE, WM_CREATE,
22        WM_CTLCOLORDLG, WM_MOVE, WM_PARENTNOTIFY, WM_SIZE,
23    },
24};
25
26use crate::{PluginApp, PluginHandler, PluginHost, sys};
27
28#[cfg(feature = "winio")]
29pub mod winio;
30
31/// TODO: Icon?
32/// TODO: Share one runtime
33#[derive(Builder)]
34pub struct OptionsPage<A: PluginApp> {
35    /// If conflicts with other plugins, Everything will append a " (plugin.dll)" suffix.
36    #[builder(into)]
37    name: String,
38    #[builder(with = |x: impl FnMut(OptionsPageLoadArgs) -> PageHandle<A> + 'static| UnsafeCell::new(Box::new(x)))]
39    load: UnsafeCell<Box<dyn FnMut(OptionsPageLoadArgs) -> PageHandle<A>>>,
40    #[builder(default)]
41    handle: UnsafeCell<Option<PageHandle<A>>>,
42}
43
44impl<A: PluginApp> OptionsPage<A> {
45    fn load_mut(&self) -> &mut dyn FnMut(OptionsPageLoadArgs) -> PageHandle<A> {
46        unsafe { &mut *self.load.get() }
47    }
48
49    fn handle(&self) -> &Option<PageHandle<A>> {
50        unsafe { &*self.handle.get() }
51    }
52
53    fn handle_mut(&self) -> &mut Option<PageHandle<A>> {
54        unsafe { &mut *self.handle.get() }
55    }
56}
57
58#[derive(Debug)]
59pub struct OptionsPageLoadArgs {
60    parent: HWND,
61}
62
63enum OptionsPageInternalMessage<A: PluginApp> {
64    Msg(OptionsPageMessage<A>),
65    Size((i32, i32)),
66    /// Map to [`WM_CLOSE`] to reuse `WindowEvent::Close`
67    Kill,
68}
69
70impl<A: PluginApp> From<OptionsPageMessage<A>> for OptionsPageInternalMessage<A> {
71    fn from(msg: OptionsPageMessage<A>) -> Self {
72        OptionsPageInternalMessage::Msg(msg)
73    }
74}
75
76impl<A: PluginApp> OptionsPageInternalMessage<A> {
77    pub fn try_into(self, window: HWND) -> Option<OptionsPageMessage<A>> {
78        match self {
79            OptionsPageInternalMessage::Msg(msg) => Some(msg),
80            OptionsPageInternalMessage::Size(v) => {
81                debug!(?v, "OptionsPageInternalMessage::Size");
82                // We do not use `SWP_NOMOVE` to mitigate the occasional misplacement bug by the way, see `winio::adjust_window` for details.
83                unsafe { SetWindowPos(window, 0 as _, 0, 0, v.0, v.1, SWP_NOZORDER) };
84                None
85            }
86            OptionsPageInternalMessage::Kill => {
87                unsafe { SendMessageW(window, WM_CLOSE, 0, 0) };
88                None
89            }
90        }
91    }
92}
93
94pub enum OptionsPageMessage<A: PluginApp> {
95    /// `(config, tx)`
96    ///
97    /// Just drop the `tx` if there is no need to save the config (i.e. no changes).
98    ///
99    /// Note [`PluginHandler::app`] is not available during saving.
100    Save(
101        &'static mut A::Config,
102        std::sync::mpsc::SyncSender<&'static mut A::Config>,
103    ),
104}
105
106impl<A: PluginApp> Debug for OptionsPageMessage<A> {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            OptionsPageMessage::Save(_config, _tx) => write!(f, "OptionsPageMessage::Save"),
110        }
111    }
112}
113
114pub struct PageHandle<A: PluginApp> {
115    #[allow(dead_code)]
116    thread_handle: std::thread::JoinHandle<()>,
117    tx: mpsc::UnboundedSender<OptionsPageInternalMessage<A>>,
118}
119
120impl<A: PluginApp> PluginHandler<A> {
121    pub fn add_options_pages(&self, data: *mut c_void) -> *mut c_void {
122        debug!("Plugin add options pages");
123        if self.options_pages.is_empty() {
124            0 as _
125        } else {
126            for (i, page) in self.options_pages.iter().enumerate() {
127                self.host()
128                    .ui_options_add_plugin_page(data, i as _, &page.name);
129            }
130            1 as _
131        }
132    }
133
134    /// Evertyhing only loads a options page when the user selects it
135    ///
136    /// TODO: `tooltip_hwnd`
137    pub fn load_options_page(&self, data: *mut c_void) -> *mut c_void {
138        debug_assert!(!self.options_pages.is_empty());
139
140        let data = unsafe { &mut *(data as *mut sys::everything_plugin_load_options_page_s) };
141        {
142            let page_hwnd = data.page_hwnd as *const c_void;
143            debug!(?page_hwnd, "Plugin load options page");
144        }
145        let page_hwnd: HWND = unsafe { mem::transmute(data.page_hwnd) };
146
147        let page = &self.options_pages[data.user_data as usize];
148
149        *page.handle_mut() = Some((page.load_mut())(OptionsPageLoadArgs { parent: page_hwnd }));
150
151        // Enable Apply button
152        // Only works when switching to the page after loading the options window
153        // self.host().ui_options_enable_or_disable_apply_button(
154        //     self.host().ui_options_from_page_hwnd(page_hwnd),
155        //     true,
156        // );
157
158        1 as _
159    }
160
161    #[cfg(feature = "winio")]
162    pub fn load_options_page_winio<'a, T: winio::OptionsPageComponent<'a>>(
163        &self,
164        data: *mut c_void,
165    ) -> *mut c_void {
166        let data = unsafe { &mut *(data as *mut sys::everything_plugin_load_options_page_s) };
167        {
168            let page_hwnd = data.page_hwnd as *const c_void;
169            debug!(?page_hwnd, "Plugin load options page");
170        }
171        let page_hwnd: HWND = unsafe { mem::transmute(data.page_hwnd) };
172
173        winio::spawn::<T>(OptionsPageLoadArgs { parent: page_hwnd });
174
175        1 as _
176    }
177
178    pub fn save_options_page(&self, data: *mut c_void) -> *mut c_void {
179        let data = unsafe { &mut *(data as *mut sys::everything_plugin_save_options_page_s) };
180        debug!(?data, "Plugin save options page");
181
182        if self.options_pages.is_empty() {
183            return 0 as _;
184        }
185
186        let page = &self.options_pages[data.user_data as usize];
187        match page.handle() {
188            Some(handle) => {
189                debug!(is_closed = handle.tx.is_closed(), "Saving options page");
190
191                let (tx, rx) = std::sync::mpsc::sync_channel(1);
192
193                let mut config = self.app_into_config();
194                let config_static: &'static mut A::Config = unsafe { mem::transmute(&mut config) };
195                match handle
196                    .tx
197                    .unbounded_send(OptionsPageMessage::Save(config_static, tx).into())
198                {
199                    Ok(()) => {
200                        if let Ok(_config) = rx.recv() {
201                            debug!(?config, "Options page config");
202                            self.app_new(Some(config));
203                        }
204                    }
205                    Err(_) => (),
206                }
207            }
208            None => warn!("Options page handle is None, can't save"),
209        }
210
211        // data.enable_apply = 1;
212        self.options_message.set(OptionsMessage::EnableApply(true));
213
214        1 as _
215    }
216
217    pub fn get_options_page_minmax(&self, _data: *mut c_void) -> *mut c_void {
218        // TODO
219        0 as _
220    }
221
222    pub fn size_options_page(&self, _data: *mut c_void) -> *mut c_void {
223        // We listen to WM_SIZE in options_page_proc instead
224        0 as _
225    }
226
227    pub fn options_page_proc(&self, data: *mut c_void) -> *mut c_void {
228        let data = unsafe { &mut *(data as *mut sys::everything_plugin_options_page_proc_s) };
229        trace!(?data, "Plugin options page proc");
230
231        if self.options_pages.is_empty() {
232            return 0 as _;
233        }
234        let page = &self.options_pages[data.user_data as usize];
235
236        let msg = data.msg as u32;
237        let w_param = data.wParam;
238        let l_param = data.lParam;
239
240        let options_hwnd = unsafe { mem::transmute(data.options_hwnd) };
241        // let page_hwnd = unsafe { mem::transmute(data.page_hwnd) };
242        // debug_assert_eq!(
243        //     self.host().ui_options_from_page_hwnd(page_hwnd),
244        //     options_hwnd
245        // );
246
247        // Plugin add options pages
248        // msg: WM_SHOWWINDOW (24), wParam: 1, lParam: 0, page_hwnd: 0x7e1b46
249        // msg: WM_WINDOWPOSCHANGING (70), wParam: 0, lParam: 15718896
250        // msg: WM_WINDOWPOSCHANGED (71), wParam: 0, lParam: 15718896
251        // msg: WM_WINDOWPOSCHANGING (70), wParam: 0, lParam: 15719520
252        // Plugin load options page page_hwnd=0x7e1b46
253        // msg: WM_PARENTNOTIFY (528), wParam: 1, lParam: 597884
254        // msg: WM_WINDOWPOSCHANGING (70), wParam: 0, lParam: 15718928
255        // msg: WM_NCCALCSIZE (131), wParam: 1, lParam: 15718880
256        // if direct: msg: WM_NEXTDLGCTL (40), wParam: 6819452, lParam: 1
257        // msg: WM_NCPAINT (133), wParam: 1, lParam: 0
258        // msg: WM_ERASEBKGND (20), wParam: 1879128502, lParam: 0
259        // msg: WM_WINDOWPOSCHANGED (71), wParam: 0, lParam: 15718928
260        // msg: WM_SIZE (5), wParam: 0, lParam: 39781242
261        // msg: WM_NCPAINT (133), wParam: 1, lParam: 0
262        // msg: WM_ERASEBKGND (20), wParam: 536950527, lParam: 0
263        // msg: WM_CTLCOLORDLG (310), wParam: 536950527, lParam: 8264518
264        // msg: WM_WINDOWPOSCHANGING (70), wParam: 0, lParam: 15716848
265        // msg: WM_PAINT (15), wParam: 0, lParam: 0
266        // msg: WM_WINDOWPOSCHANGING (70), wParam: 0, lParam: 15716848
267        // msg: WM_PAINT (15), wParam: 0, lParam: 0
268        match msg {
269            WM_MOVE | WM_CLOSE => {
270                debug!(
271                    msg,
272                    lParam = ?l_param as *const c_void,
273                    lParam = ?w_param as *const c_void,
274                );
275            }
276            WM_SIZE => {
277                if let Some(handle) = page.handle() {
278                    _ = handle.tx.unbounded_send(OptionsPageInternalMessage::Size((
279                        (l_param & 0xFFFF) as i32,
280                        (l_param >> 16) as i32,
281                    )));
282                }
283            }
284            WM_PARENTNOTIFY => {
285                debug!(wParam = data.wParam, "WM_PARENTNOTIFY");
286                match data.wParam as u32 {
287                    WM_CREATE => {
288                        // Only works when switching to the page after loading the options window
289                        // self.host()
290                        //     .ui_options_enable_or_disable_apply_button(options_hwnd, true);
291                    }
292                    _ => (),
293                }
294            }
295            WM_CTLCOLORDLG => {
296                debug!(lParam = ?data.lParam as *const c_void, "WM_CTLCOLORDLG");
297                self.host()
298                    .ui_options_enable_or_disable_apply_button(options_hwnd, true);
299            }
300            _ => (),
301        }
302
303        match self.options_message.take() {
304            OptionsMessage::Noop => (),
305            OptionsMessage::EnableApply(enable) => self
306                .host()
307                .ui_options_enable_or_disable_apply_button(options_hwnd, enable),
308        }
309
310        1 as _
311    }
312
313    pub fn kill_options_page(&self, data: *mut c_void) -> *mut c_void {
314        debug!(?data, "Plugin kill options page");
315
316        if self.options_pages.is_empty() {
317            return 0 as _;
318        }
319
320        let page = &self.options_pages[data as usize];
321        match page.handle_mut().take() {
322            Some(handle) => {
323                debug!(is_closed = handle.tx.is_closed(), "Killing options page");
324                _ = handle.tx.unbounded_send(OptionsPageInternalMessage::Kill);
325                // TODO: Without waiting can cause dangling handle
326                #[cfg(debug_assertions)]
327                std::thread::spawn(|| {
328                    // debug: ~14ms
329                    handle.thread_handle.join().unwrap();
330                    debug!("Options page thread finished");
331                });
332            }
333            None => warn!("Options page handle is None, can't kill"),
334        }
335        1 as _
336    }
337}
338
339#[derive(Default)]
340pub enum OptionsMessage {
341    #[default]
342    Noop,
343    EnableApply(bool),
344}
345
346/// `options_hwnd`
347#[repr(i32)]
348pub enum OptionsDlgItem {
349    ApplyButton = 1001,
350}
351
352impl PluginHost {
353    pub fn ui_options_add_plugin_page(
354        &self,
355        data: *mut c_void,
356        user_data: *mut c_void,
357        name: &str,
358    ) {
359        // Not in header
360        let ui_options_add_plugin_page: unsafe extern "system" fn(
361            add_custom_page: *mut c_void,
362            user_data: *mut c_void,
363            name: *const sys::everything_plugin_utf8_t,
364        )
365            -> *mut ::std::os::raw::c_void =
366            unsafe { self.get("ui_options_add_plugin_page").unwrap_unchecked() };
367        let name = CString::new(name).unwrap();
368        unsafe { ui_options_add_plugin_page(data, user_data, name.as_ptr() as _) };
369    }
370
371    pub fn ui_options_from_page_hwnd(page_hwnd: HWND) -> HWND {
372        unsafe { GetAncestor(page_hwnd, GA_ROOT) }
373    }
374
375    pub fn ui_options_enable_or_disable_apply_button(&self, options_hwnd: HWND, enable: bool) {
376        self.os_enable_or_disable_dlg_item(
377            options_hwnd,
378            OptionsDlgItem::ApplyButton as i32,
379            enable,
380        );
381    }
382
383    /// Enable or disable a dialog control.
384    ///
385    /// ## Note
386    /// For `options_hwnd`, see [`OptionsDlgItem`].
387    pub fn os_enable_or_disable_dlg_item(&self, parent_hwnd: HWND, id: i32, enable: bool) {
388        let os_enable_or_disable_dlg_item: unsafe extern "system" fn(
389            parent_hwnd: HWND,
390            id: i32,
391            enable: i32,
392        ) = unsafe { self.get("os_enable_or_disable_dlg_item").unwrap_unchecked() };
393        unsafe { os_enable_or_disable_dlg_item(parent_hwnd, id, enable as i32) };
394    }
395}