Skip to main content

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