win_task_dialog/
lib.rs

1#[cfg(windows)]
2use widestring::U16CString;
3#[cfg(windows)]
4pub use windows::{
5    core::HRESULT,
6    Win32::{
7        Foundation::{HWND, LPARAM, S_FALSE, S_OK, WPARAM},
8        UI::Controls::TASKDIALOG_NOTIFICATIONS,
9    },
10};
11#[cfg(windows)]
12use windows::{
13    core::{BOOL, PCWSTR},
14    Win32::{
15        Foundation::{FALSE, HMODULE},
16        UI::{
17            Controls::{
18                TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOGCONFIG_0, TASKDIALOGCONFIG_1,
19                TASKDIALOG_BUTTON, TASKDIALOG_COMMON_BUTTON_FLAGS, TASKDIALOG_FLAGS, TDE_CONTENT,
20                TDE_EXPANDED_INFORMATION, TDM_NAVIGATE_PAGE,
21                TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE, TDM_UPDATE_ELEMENT_TEXT,
22            },
23            WindowsAndMessaging::SendMessageA,
24        },
25    },
26};
27
28#[cfg(not(windows))]
29pub type HWND = *mut usize;
30
31#[cfg(not(windows))]
32type HMODULE = *mut usize;
33
34#[cfg(not(windows))]
35type PWSTR = *mut u16;
36
37#[cfg(not(windows))]
38type WPARAM = usize;
39
40#[cfg(not(windows))]
41type LPARAM = isize;
42
43#[cfg(not(windows))]
44type HRESULT = i32;
45
46type UINT = u32;
47
48#[cfg(not(windows))]
49#[allow(non_camel_case_types)]
50type TASKDIALOG_FLAGS = u32;
51
52#[cfg(not(windows))]
53#[allow(non_camel_case_types)]
54type TASKDIALOG_COMMON_BUTTON_FLAGS = u32;
55
56use std::{io::Error, usize};
57
58pub type TaskDialogHyperlinkCallback = Option<fn(context: &str) -> ()>;
59
60pub type TaskDialogWndProcCallback = Option<
61    unsafe extern "system" fn(
62        hwnd: HWND,
63        msg: TASKDIALOG_NOTIFICATIONS,
64        w_param: WPARAM,
65        l_param: LPARAM,
66        ref_data: *mut TaskDialogConfig,
67    ) -> HRESULT,
68>;
69
70pub enum ExecuteOption {
71    TaskDialogIndirect,
72    TaskDialogNavigate,
73}
74
75mod constants;
76pub use constants::*;
77
78pub struct TaskDialogConfig {
79    pub parent: HWND,
80    pub instance: HMODULE,
81    pub flags: TASKDIALOG_FLAGS,
82    pub common_buttons: TASKDIALOG_COMMON_BUTTON_FLAGS,
83    pub window_title: String,
84    pub main_instruction: String,
85    pub content: String,
86    pub verification_text: String,
87    pub expanded_information: String,
88    pub expanded_control_text: String,
89    pub collapsed_control_text: String,
90    pub footer: String,
91    pub buttons: Vec<TaskDialogButton>,
92    pub default_button: i32,
93    pub radio_buttons: Vec<TaskDialogButton>,
94    pub default_radio_buttons: i32,
95    pub main_icon: PCWSTR,
96    pub footer_icon: PCWSTR,
97    /** When created dialog, the value set to HWND. */
98    pub dialog_hwnd: HWND,
99    /** When close the dialog, the value set to true, default is false. */
100    pub is_destroyed: bool,
101    pub hyperlink_callback: TaskDialogHyperlinkCallback,
102    pub callback: TaskDialogWndProcCallback,
103    pub cx_width: u32,
104}
105
106impl Default for TaskDialogConfig {
107    fn default() -> Self {
108        TaskDialogConfig {
109            parent: HWND::default(),
110            instance: HMODULE::default(),
111            flags: TASKDIALOG_FLAGS::default(),
112            common_buttons: TDCBF_CANCEL_BUTTON,
113            window_title: "".to_string(),
114            main_instruction: "".to_string(),
115            content: "".to_string(),
116            verification_text: "".to_string(),
117            expanded_information: "".to_string(),
118            expanded_control_text: "".to_string(),
119            collapsed_control_text: "".to_string(),
120            footer: "".to_string(),
121            buttons: vec![],
122            default_button: 0,
123            radio_buttons: vec![],
124            default_radio_buttons: 0,
125            main_icon: PCWSTR::null(),
126            footer_icon: PCWSTR::null(),
127            dialog_hwnd: HWND::default(),
128            is_destroyed: false,
129            hyperlink_callback: None,
130            callback: None,
131            cx_width: 0,
132        }
133    }
134}
135
136#[cfg(windows)]
137impl TaskDialogConfig {
138    /// Add `TDF_SHOW_PROGRESS_BAR` flag on `marquee` is `false`,
139    /// otherwise `TDF_SHOW_MARQUEE_PROGRESS_BAR`.
140    ///
141    /// <https://docs.microsoft.com/en-us/windows/win32/controls/progress-bar-control>
142    pub fn enable_process_bar(&mut self, marquee: bool) {
143        if marquee {
144            if self.flags & TDF_SHOW_MARQUEE_PROGRESS_BAR != TDF_SHOW_MARQUEE_PROGRESS_BAR {
145                self.flags |= TDF_SHOW_MARQUEE_PROGRESS_BAR;
146            }
147        } else {
148            if self.flags & TDF_SHOW_PROGRESS_BAR != TDF_SHOW_PROGRESS_BAR {
149                self.flags |= TDF_SHOW_PROGRESS_BAR;
150            }
151        }
152    }
153
154    /// Disables progresss bar
155    pub fn disable_process_bar(&mut self, marquee: bool) {
156        if marquee {
157            if self.flags & TDF_SHOW_MARQUEE_PROGRESS_BAR == TDF_SHOW_MARQUEE_PROGRESS_BAR {
158                self.flags &= !TDF_SHOW_MARQUEE_PROGRESS_BAR;
159            }
160        } else {
161            if self.flags & TDF_SHOW_PROGRESS_BAR == TDF_SHOW_PROGRESS_BAR {
162                self.flags &= !TDF_SHOW_PROGRESS_BAR;
163            }
164        }
165    }
166
167    /// Set status or animation time of marquee progress bar
168    pub fn set_process_bar_marquee(&mut self, enable: bool, time: isize) {
169        if self.dialog_hwnd.is_invalid() {
170            return;
171        }
172        unsafe {
173            SendMessageA(
174                self.dialog_hwnd,
175                TDM_SET_PROGRESS_BAR_MARQUEE.0 as _,
176                WPARAM(if enable { 1 } else { 0 }),
177                LPARAM(time),
178            );
179        }
180    }
181
182    /// Set the percentage of the progress bar
183    pub fn set_process_bar(&mut self, percentage: usize) {
184        if self.dialog_hwnd.is_invalid() {
185            return;
186        }
187        unsafe {
188            SendMessageA(
189                self.dialog_hwnd,
190                TDM_SET_PROGRESS_BAR_POS.0 as _,
191                WPARAM(percentage),
192                LPARAM(0),
193            );
194        }
195    }
196
197    /// Set the content text
198    pub fn set_content(&mut self, content: &str) {
199        if self.dialog_hwnd.is_invalid() {
200            return;
201        }
202        self.content = content.to_string();
203        unsafe {
204            let content_wchar = U16CString::from_str_unchecked(content);
205            SendMessageA(
206                self.dialog_hwnd,
207                TDM_UPDATE_ELEMENT_TEXT.0 as _,
208                WPARAM(TDE_CONTENT.0 as _),
209                LPARAM(content_wchar.as_ptr() as _),
210            );
211        }
212    }
213
214    /// Set the main instruction text
215    pub fn set_main_instruction(&mut self, main_instruction: &str) {
216        if self.dialog_hwnd.is_invalid() {
217            return;
218        }
219        self.main_instruction = main_instruction.to_string();
220        unsafe {
221            use windows::Win32::UI::Controls::TDE_MAIN_INSTRUCTION;
222
223            let main_instruction_wchar = U16CString::from_str_unchecked(main_instruction);
224            SendMessageA(
225                self.dialog_hwnd,
226                TDM_UPDATE_ELEMENT_TEXT.0 as _,
227                WPARAM(TDE_MAIN_INSTRUCTION.0 as _),
228                LPARAM(main_instruction_wchar.as_ptr() as _),
229            );
230        }
231    }
232
233    /// Set the footer text
234    pub fn set_footer(&mut self, footer: &str) {
235        if self.dialog_hwnd.is_invalid() {
236            return;
237        }
238        self.footer = footer.to_string();
239        unsafe {
240            use windows::Win32::UI::{
241                Controls::{TDE_FOOTER, TDM_UPDATE_ELEMENT_TEXT},
242                WindowsAndMessaging::SendMessageA,
243            };
244
245            let footer_wchar = U16CString::from_str_unchecked(footer);
246            SendMessageA(
247                self.dialog_hwnd,
248                TDM_UPDATE_ELEMENT_TEXT.0 as _,
249                WPARAM(TDE_FOOTER.0 as _),
250                LPARAM(footer_wchar.as_ptr() as _),
251            );
252        }
253    }
254
255    /// Set the expanded information text
256    pub fn set_expanded_information(&mut self, expanded_information: &str) {
257        if self.dialog_hwnd.is_invalid() {
258            return;
259        }
260        self.expanded_information = expanded_information.to_string();
261        unsafe {
262            let expanded_information_wchar = U16CString::from_str_unchecked(expanded_information);
263            SendMessageA(
264                self.dialog_hwnd,
265                TDM_UPDATE_ELEMENT_TEXT.0 as _,
266                WPARAM(TDE_EXPANDED_INFORMATION.0 as _),
267                LPARAM(expanded_information_wchar.as_ptr() as _),
268            );
269        }
270    }
271
272    /// Set the button elevation state
273    pub fn set_button_elevation_required_state(&mut self, button_id: usize, enable: bool) {
274        if self.dialog_hwnd.is_invalid() {
275            return;
276        }
277        unsafe {
278            SendMessageA(
279                self.dialog_hwnd,
280                TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE.0 as _,
281                WPARAM(button_id),
282                LPARAM(if enable { 1 } else { 0 }),
283            );
284        }
285    }
286
287    /// Navigate to new page
288    pub fn navigate_page(&mut self, conf: &mut TaskDialogConfig) {
289        if self.dialog_hwnd.is_invalid() {
290            return;
291        }
292        execute_task_dialog(conf, ExecuteOption::TaskDialogNavigate).ok();
293    }
294}
295
296#[cfg(not(windows))]
297impl TaskDialogConfig {
298    pub fn enable_process_bar(&mut self, _marquee: bool) {}
299    pub fn disable_process_bar(&mut self, marquee: bool) {}
300    pub fn set_process_bar_marquee(&mut self, _enable: bool, _time: isize) {}
301    pub fn set_process_bar(&mut self, _percentage: usize) {}
302    pub fn set_content(&mut self, content: &str) {}
303    pub fn set_main_instruction(&mut self, main_instruction: &str) {}
304    pub fn set_footer(&mut self, footer: &str) {}
305    pub fn set_expanded_information(&mut self, expanded_information: &str) {}
306    pub fn set_button_elevation_required_state(&mut self, button_id: usize, enable: bool) {}
307    pub fn navigate_page(&mut self, conf: &mut TaskDialogConfig) {}
308}
309
310pub struct TaskDialogButton {
311    pub id: i32,
312    pub text: String,
313}
314
315pub struct TaskDialogResult {
316    pub button_id: i32,
317    pub radio_button_id: i32,
318    pub checked: bool,
319}
320
321impl Default for TaskDialogResult {
322    fn default() -> Self {
323        TaskDialogResult {
324            button_id: 0,
325            radio_button_id: 0,
326            checked: false,
327        }
328    }
329}
330
331/** Show task dialog */
332#[cfg(windows)]
333pub fn show_task_dialog(conf: &mut TaskDialogConfig) -> Result<TaskDialogResult, Error> {
334    execute_task_dialog(conf, ExecuteOption::TaskDialogIndirect)
335}
336
337/** Show task dialog */
338#[cfg(windows)]
339pub fn execute_task_dialog(
340    conf: &mut TaskDialogConfig,
341    opt: ExecuteOption,
342) -> Result<TaskDialogResult, Error> {
343    use std::ptr::addr_of_mut;
344
345    let mut result = TaskDialogResult::default();
346    let conf_ptr: *mut TaskDialogConfig = conf;
347    let conf_long_ptr = conf_ptr as isize;
348
349    let ret = unsafe {
350        // Call GetModuleHandleA on conf.instance is null
351
352        let instance = if conf.instance.is_invalid() {
353            use windows::Win32::System::LibraryLoader::GetModuleHandleA;
354
355            // Passing NULL handle indicates the self process handle is
356            // no way to fail in Windows
357            GetModuleHandleA(None).unwrap()
358        } else {
359            conf.instance
360        };
361
362        // Some text
363        let window_title: U16CString = U16CString::from_str_unchecked(&conf.window_title);
364        let main_instruction: U16CString = U16CString::from_str_unchecked(&conf.main_instruction);
365        let content: U16CString = U16CString::from_str_unchecked(&conf.content);
366        let verification_text: U16CString = U16CString::from_str_unchecked(&conf.verification_text);
367        let expanded_information: U16CString =
368            U16CString::from_str_unchecked(&conf.expanded_information);
369        let expanded_control_text: U16CString =
370            U16CString::from_str_unchecked(&conf.expanded_control_text);
371        let collapsed_control_text: U16CString =
372            U16CString::from_str_unchecked(&conf.collapsed_control_text);
373        let footer: U16CString = U16CString::from_str_unchecked(&conf.footer);
374
375        // Buttons
376        let btn_text: Vec<U16CString> = conf
377            .buttons
378            .iter()
379            .map(|btn| U16CString::from_str_unchecked(&btn.text))
380            .collect();
381        let buttons: Vec<TASKDIALOG_BUTTON> = conf
382            .buttons
383            .iter()
384            .enumerate()
385            .map(|(i, btn)| TASKDIALOG_BUTTON {
386                nButtonID: btn.id,
387                pszButtonText: PCWSTR(btn_text[i].as_ptr()),
388            })
389            .collect();
390
391        // Radio Buttons
392        let radio_btn_text: Vec<U16CString> = conf
393            .radio_buttons
394            .iter()
395            .map(|btn| U16CString::from_str_unchecked(&btn.text))
396            .collect();
397        let radio_buttons: Vec<TASKDIALOG_BUTTON> = conf
398            .radio_buttons
399            .iter()
400            .enumerate()
401            .map(|(i, btn)| TASKDIALOG_BUTTON {
402                nButtonID: btn.id,
403                pszButtonText: PCWSTR(radio_btn_text[i].as_ptr()),
404            })
405            .collect();
406
407        // ICON
408        let mut u1: TASKDIALOGCONFIG_0 = Default::default();
409        let mut u2: TASKDIALOGCONFIG_1 = Default::default();
410        if !conf.main_icon.is_null() {
411            u1.pszMainIcon = conf.main_icon;
412        }
413        if !conf.footer_icon.is_null() {
414            u2.pszFooterIcon = conf.footer_icon;
415        }
416
417        unsafe extern "system" fn callback(
418            hwnd: HWND,
419            msg: TASKDIALOG_NOTIFICATIONS,
420            _w_param: WPARAM,
421            _l_param: LPARAM,
422            lp_ref_data: isize,
423        ) -> HRESULT {
424            use windows::Win32::{
425                Foundation::S_OK,
426                UI::Controls::{TDN_CREATED, TDN_DESTROYED, TDN_HYPERLINK_CLICKED},
427            };
428
429            let conf = std::mem::transmute::<isize, *mut TaskDialogConfig>(lp_ref_data);
430            match msg {
431                TDN_CREATED => {
432                    (*conf).dialog_hwnd = hwnd;
433                }
434                TDN_DESTROYED => {
435                    (*conf).is_destroyed = true;
436                }
437                TDN_HYPERLINK_CLICKED => {
438                    let link = U16CString::from_ptr_str(_l_param.0 as *const u16)
439                        .to_string()
440                        .unwrap();
441                    if let Some(callback) = (*conf).hyperlink_callback {
442                        callback(&link);
443                    }
444                }
445                _ => {}
446            };
447            if let Some(callback) = (*conf).callback {
448                return callback(hwnd, msg, _w_param, _l_param, lp_ref_data as _);
449            }
450
451            S_OK
452        }
453
454        let mut config = TASKDIALOGCONFIG {
455            cbSize: std::mem::size_of::<TASKDIALOGCONFIG>() as UINT,
456            hwndParent: conf.parent,
457            hInstance: instance.into(),
458            dwFlags: conf.flags,
459            dwCommonButtons: conf.common_buttons,
460            pszWindowTitle: PCWSTR::from_raw(window_title.as_ptr()),
461            pszMainInstruction: PCWSTR::from_raw(main_instruction.as_ptr()),
462            pszContent: PCWSTR::from_raw(content.as_ptr()),
463            pszVerificationText: PCWSTR::from_raw(verification_text.as_ptr()),
464            pszExpandedInformation: PCWSTR::from_raw(expanded_information.as_ptr()),
465            pszExpandedControlText: PCWSTR::from_raw(expanded_control_text.as_ptr()),
466            pszCollapsedControlText: PCWSTR::from_raw(collapsed_control_text.as_ptr()),
467            pszFooter: PCWSTR::from_raw(footer.as_ptr()),
468            cButtons: buttons.len() as UINT,
469            pButtons: buttons.as_slice().as_ptr(),
470            nDefaultButton: conf.default_button,
471            cRadioButtons: radio_buttons.len() as UINT,
472            pRadioButtons: radio_buttons.as_slice().as_ptr(),
473            nDefaultRadioButton: conf.default_radio_buttons,
474            Anonymous1: u1,
475            Anonymous2: u2,
476            pfCallback: Some(callback),
477            lpCallbackData: conf_long_ptr,
478            cxWidth: conf.cx_width,
479        };
480
481        match opt {
482            ExecuteOption::TaskDialogIndirect => {
483                // Result
484                let mut verify: BOOL = FALSE;
485                let dialog_result = TaskDialogIndirect(
486                    &config,
487                    Some(&mut result.button_id),
488                    Some(&mut result.radio_button_id),
489                    Some(&mut verify),
490                )
491                .map_or_else(|e| e.code().0, |_| 0);
492                result.checked = verify != FALSE;
493
494                dialog_result
495            }
496            ExecuteOption::TaskDialogNavigate => {
497                SendMessageA(
498                    conf.dialog_hwnd,
499                    TDM_NAVIGATE_PAGE.0 as _,
500                    WPARAM(0),
501                    LPARAM(addr_of_mut!(config) as _),
502                );
503
504                0
505            }
506        }
507    };
508
509    if ret != 0 {
510        Err(Error::last_os_error())
511    } else {
512        Ok(result)
513    }
514}
515
516/** Show message dialog, the dialog have only the OK button */
517#[cfg(windows)]
518pub fn show_msg_dialog(
519    title: &str,
520    main_instruction: &str,
521    content: &str,
522    icon: PCWSTR,
523) -> Option<Error> {
524    let mut conf = TaskDialogConfig {
525        common_buttons: TDCBF_OK_BUTTON,
526        window_title: title.to_string(),
527        main_instruction: main_instruction.to_string(),
528        content: content.to_string(),
529        main_icon: icon,
530        ..Default::default()
531    };
532    show_task_dialog(&mut conf).err()
533}
534
535#[cfg(not(windows))]
536pub fn show_task_dialog(_conf: &TaskDialogConfig) -> Result<TaskDialogResult, Error> {
537    Ok(TaskDialogResult::default())
538}
539
540#[cfg(not(windows))]
541pub fn show_msg_dialog(
542    _title: &str,
543    _main_instruction: &str,
544    _content: &str,
545    _icon: *mut u16,
546) -> Option<Error> {
547    None
548}