Remove winsafe crate and migrate everything to windows-rs

This commit is contained in:
Caelan Sayler
2025-05-24 00:59:14 +01:00
parent a0cca6afbc
commit 3c7d82aa22
13 changed files with 612 additions and 492 deletions

View File

@@ -63,7 +63,6 @@
{
"groupName": "frozen",
"matchPackageNames": [
"winsafe", // newer versions causes runtime errors in release builds
"System.CommandLine", // too many breaking changes too frequently
"xunit.runner.visualstudio", // 20-12-2024: broke tests (something about sn signing maybe?)
"Microsoft.NET.Test.Sdk", // 23-05-2025: 17.13.0 was the last version which supported net6

18
Cargo.lock generated
View File

@@ -2328,8 +2328,8 @@ dependencies = [
"walkdir",
"webview2-com-sys",
"windows",
"winreg",
"winres",
"winsafe",
"zip",
"zstd",
]
@@ -2844,6 +2844,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if 1.0.0",
"windows-sys 0.59.0",
]
[[package]]
name = "winres"
version = "0.1.12"
@@ -2853,12 +2863,6 @@ dependencies = [
"toml 0.5.11",
]
[[package]]
name = "winsafe"
version = "0.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40369220be405a294b88b13ccc3d916fac12423250b30092c8c4ea19001f7f1"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"

View File

@@ -77,7 +77,6 @@ fs_extra = "1.3"
memmap2 = "0.9"
windows = "0.61"
webview2-com-sys = "0.37"
winsafe = { version = "0.0.20", features = ["gui"] }
cbindgen = "0.28"
log-panics = "2.1.0"
core-foundation = "0.10"
@@ -87,9 +86,7 @@ walkdir = "2.5"
rayon = "1.6"
progress-streams = "1.1"
flate2 = { version = "1.0", default-features = false }
# mtzip = "=4.0.2"
# ripunzip = "=2.0.1"
# zerofrom = "=0.1.5"
winreg = "0.55"
# default to small, optimized workspace release binaries
[profile.release]

View File

@@ -85,7 +85,6 @@ waitpid-any.workspace = true
fs_extra.workspace = true
memmap2.workspace = true
image.workspace = true
winsafe.workspace = true
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
@@ -93,23 +92,26 @@ windows = { workspace = true, features = [
"Win32_Globalization",
"Win32_UI",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Controls",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_System_Threading",
"Win32_System_SystemInformation",
"Win32_System_Variant",
"Win32_System_Environment",
"Win32_Storage_EnhancedStorage",
"Win32_Storage_FileSystem",
"Win32_System_Com_StructuredStorage",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_System_ProcessStatus",
"Win32_System_WindowsProgramming",
"Win32_System_LibraryLoader",
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
"Win32_System_ApplicationInstallationAndServicing",
"Win32_System_Kernel",
"Win32_Storage_EnhancedStorage",
"Win32_Storage_FileSystem",
"Wdk",
"Wdk_System",
"Wdk_System_Threading",
@@ -118,6 +120,7 @@ webview2-com-sys.workspace = true
libloading.workspace = true
strsim.workspace = true
same-file.workspace = true
winreg.workspace = true
# filelocksmith.workspace = true
[dev-dependencies]

View File

@@ -79,7 +79,7 @@ fn test_show_all_dialogs() {
show_warn("Warning", None, "This is a warning.");
show_info("Information", None, "This is information.");
assert!(show_ok_cancel("Ok/Cancel", None, "This is a question.", None));
assert!(!show_ok_cancel("Ok/Cancel", None, "This is a question.", Some("Ok")));
assert!(!show_ok_cancel("Ok/Cancel", None, "This is a question.", Some("Dont click!")));
}
// pub fn yes_no(title: &str, header: Option<&str>, body: &str) -> Result<bool> {

View File

@@ -37,25 +37,26 @@ pub enum DialogResult {
#[cfg(target_os = "windows")]
impl DialogButton {
pub fn to_win(&self) -> winsafe::co::TDCBF {
let mut result = unsafe { winsafe::co::TDCBF::from_raw(0) };
pub fn to_win(&self) -> windows::Win32::UI::Controls::TASKDIALOG_COMMON_BUTTON_FLAGS {
use windows::Win32::UI::Controls::*;
let mut result = TASKDIALOG_COMMON_BUTTON_FLAGS(0);
if self.has_ok() {
result |= winsafe::co::TDCBF::OK;
result |= TDCBF_OK_BUTTON;
}
if self.has_yes() {
result |= winsafe::co::TDCBF::YES;
result |= TDCBF_YES_BUTTON;
}
if self.has_no() {
result |= winsafe::co::TDCBF::NO;
result |= TDCBF_NO_BUTTON;
}
if self.has_cancel() {
result |= winsafe::co::TDCBF::CANCEL;
result |= TDCBF_CANCEL_BUTTON;
}
if self.has_retry() {
result |= winsafe::co::TDCBF::RETRY;
result |= TDCBF_RETRY_BUTTON;
}
if self.has_close() {
result |= winsafe::co::TDCBF::CLOSE;
result |= TDCBF_CLOSE_BUTTON;
}
result
}
@@ -63,28 +64,31 @@ impl DialogButton {
impl DialogIcon {
#[cfg(target_os = "windows")]
pub fn to_win(&self) -> winsafe::co::TD_ICON {
pub fn to_win(&self) -> windows::core::PCWSTR {
use windows::Win32::UI::Controls::*;
match self {
DialogIcon::Warning => winsafe::co::TD_ICON::WARNING,
DialogIcon::Error => winsafe::co::TD_ICON::ERROR,
DialogIcon::Information => winsafe::co::TD_ICON::INFORMATION,
DialogIcon::Warning => TD_WARNING_ICON,
DialogIcon::Error => TD_ERROR_ICON,
DialogIcon::Information => TD_INFORMATION_ICON,
}
}
}
#[cfg(target_os = "windows")]
impl DialogResult {
pub fn from_win(dlg_id: winsafe::co::DLGID) -> DialogResult {
pub fn from_win(dlg_id: i32) -> DialogResult {
use windows::Win32::UI::WindowsAndMessaging::*;
let dlg_id = MESSAGEBOX_RESULT(dlg_id);
match dlg_id {
winsafe::co::DLGID::OK => DialogResult::Ok,
winsafe::co::DLGID::CANCEL => DialogResult::Cancel,
winsafe::co::DLGID::ABORT => DialogResult::Abort,
winsafe::co::DLGID::RETRY => DialogResult::Retry,
winsafe::co::DLGID::IGNORE => DialogResult::Ignore,
winsafe::co::DLGID::YES => DialogResult::Yes,
winsafe::co::DLGID::NO => DialogResult::No,
winsafe::co::DLGID::TRYAGAIN => DialogResult::Tryagain,
winsafe::co::DLGID::CONTINUE => DialogResult::Continue,
IDOK => DialogResult::Ok,
IDCANCEL => DialogResult::Cancel,
IDABORT => DialogResult::Abort,
IDRETRY => DialogResult::Retry,
IDIGNORE => DialogResult::Ignore,
IDYES => DialogResult::Yes,
IDNO => DialogResult::No,
IDTRYAGAIN => DialogResult::Tryagain,
IDCONTINUE => DialogResult::Continue,
_ => DialogResult::Unknown,
}
}

View File

@@ -1,9 +1,22 @@
use super::{dialogs_common::*, dialogs_const::*};
use velopack::bundle::Manifest;
use crate::windows::strings::string_to_wide;
use anyhow::Result;
use std::path::PathBuf;
use winsafe::{self as w, co, prelude::*, WString};
use velopack::locator::{auto_locate_app_manifest, LocationContext};
use velopack::{
bundle::Manifest,
locator::{auto_locate_app_manifest, LocationContext},
};
use windows::{
core::HRESULT,
Win32::{
Foundation::{FALSE, HWND, LPARAM, S_FALSE, S_OK, WPARAM},
UI::{
Controls::*,
Shell::ShellExecuteW,
WindowsAndMessaging::{GetDesktopWindow, IDCANCEL, IDCONTINUE, IDOK, IDRETRY, IDYES, SW_SHOWDEFAULT},
},
},
};
pub fn show_restart_required(app: &Manifest) {
show_warn(
@@ -34,7 +47,7 @@ pub fn show_update_missing_dependencies_dialog(
"{} {to} has missing dependencies which need to be installed: {}, would you like to continue?",
app.title, depedency_string
)
.as_str(),
.as_str(),
Some("Install & Update"),
)
}
@@ -58,32 +71,33 @@ pub fn show_uninstall_complete_with_errors_dialog(app_title: &str, log_path: Opt
return;
}
let mut setup_name = WString::from_str(format!("{} Uninstall", app_title));
let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app_title));
let mut content = WString::from_str(
let setup_name = string_to_wide(format!("{} Uninstall", app_title));
let instruction = string_to_wide(format!("{} uninstall has completed with errors.", app_title));
let content = string_to_wide(
"There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again.",
);
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.dwFlags = co::TDF::ENABLE_HYPERLINKS | co::TDF::SIZE_TO_CONTENT;
config.dwCommonButtons = co::TDCBF::OK;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::WARNING));
config.set_pszWindowTitle(Some(&mut setup_name));
config.set_pszMainInstruction(Some(&mut instruction));
config.set_pszContent(Some(&mut content));
let mut config = TASKDIALOGCONFIG::default();
config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_SIZE_TO_CONTENT;
config.dwCommonButtons = TDCBF_OK_BUTTON;
config.pszWindowTitle = setup_name.as_pcwstr();
config.pszMainInstruction = instruction.as_pcwstr();
config.pszContent = content.as_pcwstr();
config.Anonymous1.pszMainIcon = TD_WARNING_ICON;
let footer_path = log_path.map(|p| p.to_string_lossy().to_string()).unwrap_or("".to_string());
let mut footer = WString::from_str(format!("Log file: '<A HREF=\"na\">{}</A>'", footer_path));
let footer = string_to_wide(format!("Log file: '<A HREF=\"na\">{}</A>'", footer_path));
if let Some(log_path) = log_path {
if log_path.exists() {
config.set_pszFooterIcon(w::IconId::Id(co::TD_ICON::INFORMATION.into()));
config.set_pszFooter(Some(&mut footer));
config.lpCallbackData = log_path as *const PathBuf as usize;
config.Anonymous2.pszFooterIcon = TD_INFORMATION_ICON;
config.pszFooter = footer.as_pcwstr();
config.lpCallbackData = log_path as *const PathBuf as isize;
config.pfCallback = Some(task_dialog_callback);
}
}
let _ = w::TaskDialogIndirect(&config, None);
unsafe { TaskDialogIndirect(&config, None, None, None).ok() };
}
pub fn show_processes_locking_folder_dialog(app_title: &str, app_version: &str, process_names: &str) -> DialogResult {
@@ -91,42 +105,38 @@ pub fn show_processes_locking_folder_dialog(app_title: &str, app_version: &str,
return DialogResult::Cancel;
}
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
let mut config = TASKDIALOGCONFIG::default();
config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
config.Anonymous1.pszMainIcon = TD_INFORMATION_ICON;
let mut update_name = WString::from_str(format!("{} Update {}", app_title, app_version));
let mut instruction = WString::from_str(format!("{} Update", app_title));
let mut content = WString::from_str(format!(
let update_name = string_to_wide(format!("{} Update {}", app_title, app_version));
let instruction = string_to_wide(format!("{} Update", app_title));
let content = string_to_wide(format!(
"There are programs ({}) preventing the {} update from proceeding. \n\n\
You can press Continue to have this updater attempt to close them automatically, or if you've closed them yourself press Retry for the updater to check again.",
process_names, app_title));
let mut btn_retry_txt = WString::from_str("Retry\nTry again if you've closed the program(s)");
let mut btn_continue_txt = WString::from_str("Continue\nAttempt to close the program(s) automatically");
let mut btn_cancel_txt = WString::from_str("Cancel\nThe update will not continue");
let btn_retry_txt = string_to_wide("Retry\nTry again if you've closed the program(s)");
let btn_continue_txt = string_to_wide("Continue\nAttempt to close the program(s) automatically");
let btn_cancel_txt = string_to_wide("Cancel\nThe update will not continue");
let btn_retry = TASKDIALOG_BUTTON { nButtonID: IDRETRY.0, pszButtonText: btn_retry_txt.as_pcwstr() };
let btn_continue = TASKDIALOG_BUTTON { nButtonID: IDCONTINUE.0, pszButtonText: btn_continue_txt.as_pcwstr() };
let btn_cancel = TASKDIALOG_BUTTON { nButtonID: IDCANCEL.0, pszButtonText: btn_cancel_txt.as_pcwstr() };
let custom_btns = vec![btn_retry, btn_continue, btn_cancel];
let mut btn_retry = w::TASKDIALOG_BUTTON::default();
btn_retry.set_nButtonID(co::DLGID::RETRY.into());
btn_retry.set_pszButtonText(Some(&mut btn_retry_txt));
config.dwFlags = TDF_USE_COMMAND_LINKS;
config.cButtons = custom_btns.len() as u32;
config.pButtons = custom_btns.as_ptr();
config.pszWindowTitle = update_name.as_pcwstr();
config.pszMainInstruction = instruction.as_pcwstr();
config.pszContent = content.as_pcwstr();
let mut btn_continue = w::TASKDIALOG_BUTTON::default();
btn_continue.set_nButtonID(co::DLGID::CONTINUE.into());
btn_continue.set_pszButtonText(Some(&mut btn_continue_txt));
let mut pnbutton = 0;
let mut pnradiobutton = 0;
let mut pfverificationflagchecked = FALSE;
let mut btn_cancel = w::TASKDIALOG_BUTTON::default();
btn_cancel.set_nButtonID(co::DLGID::CANCEL.into());
btn_cancel.set_pszButtonText(Some(&mut btn_cancel_txt));
let mut custom_btns = vec![btn_retry, btn_continue, btn_cancel];
config.dwFlags = co::TDF::USE_COMMAND_LINKS;
config.set_pButtons(Some(&mut custom_btns));
config.set_pszWindowTitle(Some(&mut update_name));
config.set_pszMainInstruction(Some(&mut instruction));
config.set_pszContent(Some(&mut content));
let (btn, _) = w::TaskDialogIndirect(&config, None).ok().unwrap_or((co::DLGID::CANCEL, 0));
DialogResult::from_win(btn)
unsafe { TaskDialogIndirect(&config, Some(&mut pnbutton), Some(&mut pnradiobutton), Some(&mut pfverificationflagchecked)).ok() };
DialogResult::from_win(pnbutton)
}
pub fn show_overwrite_repair_dialog(app: &Manifest, root_path: &PathBuf, root_is_default: bool) -> bool {
@@ -134,79 +144,75 @@ pub fn show_overwrite_repair_dialog(app: &Manifest, root_path: &PathBuf, root_is
return true;
}
// these are the defaults, if we can't detect the current app version - we call it "Repair"
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::WARNING));
let mut config = TASKDIALOGCONFIG::default();
config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
config.Anonymous1.pszMainIcon = TD_WARNING_ICON;
let mut setup_name = WString::from_str(format!("{} Setup {}", app.title, app.version));
let mut instruction = WString::from_str(format!("{} is already installed.", app.title));
let mut content = WString::from_str(
"This application is installed on your computer. If it is not functioning correctly, you can attempt to repair it.",
);
let mut btn_yes_txt = WString::from_str(format!("Repair\nErase the application and re-install version {}.", app.version));
let mut btn_cancel_txt = WString::from_str("Cancel\nBackup or save your work first");
let setup_name = string_to_wide(format!("{} Setup {}", app.title, app.version));
let mut instruction = string_to_wide(format!("{} is already installed.", app.title));
let mut content =
string_to_wide("This application is installed on your computer. If it is not functioning correctly, you can attempt to repair it.");
let mut btn_yes_txt = string_to_wide(format!("Repair\nErase the application and re-install version {}.", app.version));
let btn_cancel_txt = string_to_wide("Cancel\nBackup or save your work first");
// if we can detect the current app version, we call it "Update" or "Downgrade"
let old_app = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(root_path.to_owned()));
if let Ok(old) = old_app {
let old_version = old.get_manifest_version();
if old_version < app.version {
instruction = WString::from_str(format!("An older version of {} is installed.", app.title));
content = WString::from_str(format!("Would you like to update from {} to {}?", old_version, app.version));
btn_yes_txt = WString::from_str(format!("Update\nTo version {}", app.version));
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
instruction = string_to_wide(format!("An older version of {} is installed.", app.title));
content = string_to_wide(format!("Would you like to update from {} to {}?", old_version, app.version));
btn_yes_txt = string_to_wide(format!("Update\nTo version {}", app.version));
config.Anonymous1.pszMainIcon = TD_INFORMATION_ICON;
} else if old_version > app.version {
instruction = WString::from_str(format!("A newer version of {} is installed.", app.title));
content = WString::from_str(format!(
instruction = string_to_wide(format!("A newer version of {} is installed.", app.title));
content = string_to_wide(format!(
"You already have {} installed. Would you like to downgrade this application to an older version?",
old_version
));
btn_yes_txt = WString::from_str(format!("Downgrade\nTo version {}", app.version));
btn_yes_txt = string_to_wide(format!("Downgrade\nTo version {}", app.version));
}
}
let mut footer = if root_is_default {
WString::from_str(format!("The install directory is '<A HREF=\"na\">%LocalAppData%\\{}</A>'", app.id))
let footer = if root_is_default {
string_to_wide(format!("The install directory is '<A HREF=\"na\">%LocalAppData%\\{}</A>'", app.id))
} else {
WString::from_str(format!("The install directory is '<A HREF=\"na\">{}</A>'", root_path.display()))
string_to_wide(format!("The install directory is '<A HREF=\"na\">{}</A>'", root_path.display()))
};
let mut btn_yes = w::TASKDIALOG_BUTTON::default();
btn_yes.set_nButtonID(co::DLGID::YES.into());
btn_yes.set_pszButtonText(Some(&mut btn_yes_txt));
let btn_yes = TASKDIALOG_BUTTON { nButtonID: IDYES.0, pszButtonText: btn_yes_txt.as_pcwstr() };
let btn_cancel = TASKDIALOG_BUTTON { nButtonID: IDCANCEL.0, pszButtonText: btn_cancel_txt.as_pcwstr() };
let custom_btns = vec![btn_yes, btn_cancel];
let mut btn_cancel = w::TASKDIALOG_BUTTON::default();
btn_cancel.set_nButtonID(co::DLGID::CANCEL.into());
btn_cancel.set_pszButtonText(Some(&mut btn_cancel_txt));
config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_USE_COMMAND_LINKS;
config.cButtons = custom_btns.len() as u32;
config.pButtons = custom_btns.as_ptr();
config.pszWindowTitle = setup_name.as_pcwstr();
config.pszMainInstruction = instruction.as_pcwstr();
config.pszContent = content.as_pcwstr();
config.Anonymous2.pszFooterIcon = TD_INFORMATION_ICON;
config.pszFooter = footer.as_pcwstr();
let mut custom_btns = Vec::with_capacity(2);
custom_btns.push(btn_yes);
custom_btns.push(btn_cancel);
config.dwFlags = co::TDF::ENABLE_HYPERLINKS | co::TDF::USE_COMMAND_LINKS;
config.set_pButtons(Some(&mut custom_btns));
config.set_pszWindowTitle(Some(&mut setup_name));
config.set_pszMainInstruction(Some(&mut instruction));
config.set_pszContent(Some(&mut content));
config.set_pszFooterIcon(w::IconId::Id(co::TD_ICON::INFORMATION.into()));
config.set_pszFooter(Some(&mut footer));
config.lpCallbackData = root_path as *const PathBuf as usize;
config.lpCallbackData = root_path as *const PathBuf as isize;
config.pfCallback = Some(task_dialog_callback);
let (btn, _) = w::TaskDialogIndirect(&config, None).ok().unwrap_or_else(|| (co::DLGID::YES, 0));
return btn == co::DLGID::YES;
let mut pnbutton = 0;
let mut pnradiobutton = 0;
let mut pfverificationflagchecked = FALSE;
unsafe { TaskDialogIndirect(&config, Some(&mut pnbutton), Some(&mut pnradiobutton), Some(&mut pfverificationflagchecked)).ok() };
pnbutton == IDYES.0
}
extern "system" fn task_dialog_callback(_: w::HWND, msg: co::TDN, _: usize, _: isize, lp_ref_data: usize) -> co::HRESULT {
if msg == co::TDN::HYPERLINK_CLICKED {
extern "system" fn task_dialog_callback(_hwnd: HWND, msg: TASKDIALOG_NOTIFICATIONS, _: WPARAM, _: LPARAM, lp_ref_data: isize) -> HRESULT {
if msg == TDN_HYPERLINK_CLICKED {
let raw = lp_ref_data as *const PathBuf;
let path: &PathBuf = unsafe { &*raw };
let dir = path.to_str().unwrap();
w::HWND::GetDesktopWindow().ShellExecute("open", &dir, None, None, co::SW::SHOWDEFAULT).ok();
return co::HRESULT::S_FALSE; // do not close dialog
let dir = path.to_string_lossy().to_string();
let dir = string_to_wide(dir);
unsafe { ShellExecuteW(Some(GetDesktopWindow()), None, dir.as_pcwstr(), None, None, SW_SHOWDEFAULT) };
return S_FALSE; // do not close dialog
}
return co::HRESULT::S_OK; // close dialog on button press
return S_OK; // close dialog on button press
}
pub fn generate_confirm(
@@ -217,42 +223,40 @@ pub fn generate_confirm(
btns: DialogButton,
ico: DialogIcon,
) -> Result<DialogResult> {
let hparent = w::HWND::GetDesktopWindow();
let mut ok_text_buf = WString::from_opt_str(ok_text);
let mut custom_btns = if ok_text.is_some() {
let mut td_btn = w::TASKDIALOG_BUTTON::default();
td_btn.set_nButtonID(co::DLGID::OK.into());
td_btn.set_pszButtonText(Some(&mut ok_text_buf));
let mut custom_btns = Vec::with_capacity(1);
custom_btns.push(td_btn);
custom_btns
let hparent = unsafe { GetDesktopWindow() };
let mut ok_text_buf = ok_text.map(string_to_wide);
let mut custom_btns = if let Some(ok_text_buf) = ok_text_buf.as_mut() {
let td_btn = TASKDIALOG_BUTTON { nButtonID: IDOK.0, pszButtonText: ok_text_buf.as_pcwstr() };
vec![td_btn]
} else {
Vec::<w::TASKDIALOG_BUTTON>::default()
Vec::new()
};
let mut tdc = w::TASKDIALOGCONFIG::default();
tdc.hwndParent = unsafe { hparent.raw_copy() };
tdc.dwFlags = co::TDF::ALLOW_DIALOG_CANCELLATION | co::TDF::POSITION_RELATIVE_TO_WINDOW;
let mut tdc = TASKDIALOGCONFIG { hwndParent: hparent, ..Default::default() };
tdc.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_POSITION_RELATIVE_TO_WINDOW;
tdc.dwCommonButtons = btns.to_win();
tdc.set_pszMainIcon(w::IconIdTdicon::Tdicon(ico.to_win()));
tdc.Anonymous1.pszMainIcon = ico.to_win();
if ok_text.is_some() {
tdc.set_pButtons(Some(&mut custom_btns));
if !custom_btns.is_empty() {
tdc.cButtons = custom_btns.len() as u32;
tdc.pButtons = custom_btns.as_mut_ptr();
}
let mut title_buf = WString::from_str(title);
tdc.set_pszWindowTitle(Some(&mut title_buf));
let title_buf = string_to_wide(title);
tdc.pszWindowTitle = title_buf.as_pcwstr();
let mut header_buf = WString::from_opt_str(header);
if header.is_some() {
tdc.set_pszMainInstruction(Some(&mut header_buf));
let mut header_buf = header.map(string_to_wide);
if let Some(header_buf) = header_buf.as_mut() {
tdc.pszMainInstruction = header_buf.as_pcwstr();
}
let mut body_buf = WString::from_str(body);
tdc.set_pszContent(Some(&mut body_buf));
let body_buf = string_to_wide(body);
tdc.pszContent = body_buf.as_pcwstr();
let result = w::TaskDialogIndirect(&tdc, None).map(|(dlg_id, _)| dlg_id)?;
Ok(DialogResult::from_win(result))
let mut pnbutton = 0;
unsafe { TaskDialogIndirect(&tdc, Some(&mut pnbutton), None, None).expect("didnt work") };
Ok(DialogResult::from_win(pnbutton))
}
pub fn generate_alert(
@@ -263,6 +267,28 @@ pub fn generate_alert(
btns: DialogButton,
ico: DialogIcon,
) -> Result<()> {
let _ = generate_confirm(title, header, body, ok_text, btns, ico).map(|_| ())?;
let _ = generate_confirm(title, header, body, ok_text, btns, ico)?;
Ok(())
}
#[ignore]
#[test]
fn show_all_windows_dialogs() {
use semver::Version;
let app = Manifest {
id: "test.app".to_string(),
title: "Test Application".to_string(),
version: semver::Version::new(1, 0, 0),
description: "A test application for dialog generation.".to_string(),
authors: "Test Author".to_string(),
runtime_dependencies: "net8-x64".to_string(),
..Default::default()
};
show_restart_required(&app);
show_update_missing_dependencies_dialog(&app, "net8-x64", &Version::new(1, 0, 0), &Version::new(2, 0, 0));
show_setup_missing_dependencies_dialog(&app, "net8-x64");
show_uninstall_complete_with_errors_dialog("Test Application", Some(&PathBuf::from("C:\\audio.log")));
show_processes_locking_folder_dialog(&app.title, &app.version.to_string(), "TestProcess1, TestProcess2");
show_overwrite_repair_dialog(&app, &PathBuf::from("C:\\Program Files\\TestApp"), false);
}

View File

@@ -4,6 +4,7 @@ pub mod prerequisite;
pub mod runtimes;
pub mod splash;
pub mod known_path;
pub mod strings;
pub mod registry;
pub mod webview2;

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use chrono::{Datelike, Local as DateTime};
use velopack::locator::VelopackLocator;
use winsafe::{self as w, co, prelude::*};
use winreg::{enums::*, RegKey};
const UNINSTALL_REGISTRY_KEY: &'static str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
@@ -17,6 +17,7 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
let updater_path = locator.get_update_path_as_string();
let folder_size = fs_extra::dir::get_size(locator.get_current_bin_dir()).unwrap_or(0);
let folder_size_kb = folder_size / 1024;
let short_version = locator.get_manifest_version_short_string();
let now = DateTime::now();
@@ -25,29 +26,33 @@ pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet: String = format!("\"{}\" --uninstall --silent", updater_path);
let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
let reg_app = reg_uninstall.RegCreateKeyEx(&app_id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(app_title))?;
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(short_version))?;
reg_app.RegSetKeyValue(None, Some("InstallDate"), w::RegistryValue::Sz(formatted_date))?;
reg_app.RegSetKeyValue(None, Some("InstallLocation"), w::RegistryValue::Sz(root_path_str))?;
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(app_authors))?;
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?;
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?;
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?;
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (reg_uninstall, _reg_uninstall_disp) = hkcu.create_subkey(UNINSTALL_REGISTRY_KEY)?;
let (reg_app, _reg_app_disp) = reg_uninstall.create_subkey(&app_id)?;
let u32true = 1u32;
let language = 0x0409u32;
reg_app.set_value("DisplayIcon", &main_exe_path)?;
reg_app.set_value("DisplayName", &app_title)?;
reg_app.set_value("DisplayVersion", &short_version)?;
reg_app.set_value("InstallDate", &formatted_date)?;
reg_app.set_value("InstallLocation", &root_path_str)?;
reg_app.set_value("Publisher", &app_authors)?;
reg_app.set_value("QuietUninstallString", &uninstall_quiet)?;
reg_app.set_value("UninstallString", &uninstall_cmd)?;
reg_app.set_value("EstimatedSize", &folder_size_kb)?;
reg_app.set_value("NoModify", &u32true)?;
reg_app.set_value("NoRepair", &u32true)?;
reg_app.set_value("Language", &language)?;
Ok(())
}
pub fn remove_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
info!("Removing uninstall registry keys...");
let app_id = locator.get_manifest_id();
let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
reg_uninstall.RegDeleteKey(&app_id)?;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (reg_uninstall, _reg_uninstall_disp) = hkcu.create_subkey(UNINSTALL_REGISTRY_KEY)?;
reg_uninstall.delete_subkey_all(&app_id)?;
Ok(())
}

View File

@@ -4,7 +4,7 @@ use regex::Regex;
use std::process::Command as Process;
use std::{collections::HashMap, fs, path::Path};
use velopack::download;
use winsafe::{self as w, co, prelude::*};
use winreg::{enums::*, RegKey};
const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe";
const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
@@ -129,20 +129,17 @@ impl RuntimeInfo for FullFrameworkInfo {
}
fn is_installed(&self) -> bool {
let lm = w::HKEY::LOCAL_MACHINE;
let key = lm.RegOpenKeyEx(Some(NDP_REG_KEY), co::REG_OPTION::NoValue, co::KEY::READ);
if key.is_err() {
// key doesn't exist, so .net framework not installed
return false;
}
let release = key.unwrap().RegGetValue(None, Some("Release"));
if release.is_err() {
// key doesn't exist, so .net framework not installed
return false;
}
match release.unwrap() {
w::RegistryValue::Dword(v) => return v >= self.release_version,
_ => return false,
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
match hklm.open_subkey(NDP_REG_KEY) {
Ok(ndp) => {
let dword_val: std::io::Result<u32> = ndp.get_value("Release");
if let Ok(rel) = dword_val {
rel >= self.release_version
} else {
false // key doesn't exist, so .net framework not installed
}
}
Err(_) => false, // key doesn't exist, so .net framework not installed
}
}
@@ -186,8 +183,8 @@ impl RuntimeInfo for VCRedistInfo {
fn is_installed(&self) -> bool {
let mut installed_programs = HashMap::new();
get_installed_programs(&mut installed_programs, co::KEY::READ | co::KEY::WOW64_32KEY);
get_installed_programs(&mut installed_programs, co::KEY::READ | co::KEY::WOW64_64KEY);
get_installed_programs(&mut installed_programs, KEY_READ | KEY_WOW64_32KEY);
get_installed_programs(&mut installed_programs, KEY_READ | KEY_WOW64_64KEY);
let (my_major, my_minor, my_build, _) = util::parse_version(&self.min_version).unwrap();
let reg = Regex::new(r"(?i)Microsoft Visual C\+\+(.*)Redistributable").unwrap();
for (k, v) in installed_programs {
@@ -212,26 +209,22 @@ impl RuntimeInfo for VCRedistInfo {
}
}
fn get_installed_programs(map: &mut HashMap<String, String>, access_rights: co::KEY) {
let key = w::HKEY::LOCAL_MACHINE.RegOpenKeyEx(Some(UNINSTALL_REG_KEY), co::REG_OPTION::NoValue, access_rights);
fn get_installed_programs(map: &mut HashMap<String, String>, access_rights: u32) {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = hklm.open_subkey_with_flags(UNINSTALL_REG_KEY, access_rights);
if let Ok(view) = key {
if let Ok(iter) = view.RegEnumKeyEx() {
for key_result in iter {
if let Ok(key_name) = key_result {
let subkey = view.RegOpenKeyEx(Some(&key_name), co::REG_OPTION::NoValue, access_rights);
if subkey.is_err() {
continue;
}
let subkey = subkey.unwrap();
let name = subkey.RegQueryValueEx(Some("DisplayName"));
let version = subkey.RegQueryValueEx(Some("DisplayVersion"));
if name.is_ok() && version.is_ok() {
if let w::RegistryValue::Sz(display_name) = name.unwrap() {
if let w::RegistryValue::Sz(display_version) = version.unwrap() {
map.insert(display_name, display_version);
}
}
}
for key_result in view.enum_keys() {
if let Ok(key_name) = key_result {
let subkey = view.open_subkey_with_flags(key_name, access_rights);
if subkey.is_err() {
continue;
}
let subkey = subkey.unwrap();
let name: std::io::Result<String> = subkey.get_value("DisplayName");
let version: std::io::Result<String> = subkey.get_value("DisplayVersion");
if name.is_ok() && version.is_ok() {
map.insert(name.unwrap(), version.unwrap());
}
}
}
@@ -241,7 +234,7 @@ fn get_installed_programs(map: &mut HashMap<String, String>, access_rights: co::
#[test]
fn test_get_installed_programs_returns_visual_studio() {
let mut map = HashMap::new();
get_installed_programs(&mut map, co::KEY::READ | co::KEY::WOW64_64KEY);
get_installed_programs(&mut map, KEY_READ | KEY_WOW64_64KEY);
assert!(map.contains_key("Microsoft Visual Studio Installer"));
}
@@ -557,7 +550,11 @@ impl RuntimeInfo for WebView2Info {
}
fn install(&self, installer_path: &str, quiet: bool) -> Result<RuntimeInstallResult> {
let args = if quiet { vec!["/silent", "/install"] } else { vec!["/install"] };
let args = if quiet {
vec!["/silent", "/install"]
} else {
vec!["/install"]
};
info!("Running installer: '{}', args={:?}", installer_path, args);
let mut cmd = Process::new(installer_path).args(&args).spawn()?;

View File

@@ -1,16 +1,17 @@
use super::strings::string_to_wide;
use anyhow::{bail, Result};
use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, ImageFormat, ImageReader};
use std::sync::atomic::{AtomicI16, Ordering};
use std::{
cell::RefCell,
io::Cursor,
ops::Deref,
rc::Rc,
sync::mpsc::{self, Receiver, Sender},
thread,
use std::sync::mpsc::{self, Receiver, Sender};
use std::{io::Cursor, thread};
use windows::{
core::HRESULT,
Win32::{
Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, S_OK, WPARAM},
Graphics::Gdi::*,
System::LibraryLoader::GetModuleHandleW,
UI::{Controls::*, WindowsAndMessaging::*},
},
};
use winsafe::guard::DeleteObjectGuard;
use winsafe::{self as w, co, gui, prelude::*, WString};
const TMR_GIF: usize = 1;
const MSG_NOMESSAGE: i16 = -99;
@@ -33,10 +34,9 @@ pub fn show_splash_dialog(app_name: String, imgstream: Option<Vec<u8>>) -> Sende
thread::spawn(move || {
info!("Showing splash screen immediately...");
if imgstream.is_some() {
let _ = SplashWindow::new(app_name, imgstream.unwrap(), rx).and_then(|w| {
w.run()?;
Ok(())
});
if let Err(e) = unsafe { SplashWindow::run(app_name, imgstream.unwrap(), rx) } {
error!("Failed to show splash screen: {:?}", e);
}
} else {
let setup_name = format!("{} Setup", app_name);
let content = format!("Installing {}...", app_name);
@@ -46,21 +46,19 @@ pub fn show_splash_dialog(app_name: String, imgstream: Option<Vec<u8>>) -> Sende
tx
}
#[derive(Clone)]
pub struct SplashWindow {
wnd: gui::WindowMain,
frames: Rc<Vec<DeleteObjectGuard<w::HBITMAP>>>,
rx: Rc<Receiver<i16>>,
delay: u16,
progress: Rc<RefCell<i16>>,
frame_idx: Rc<RefCell<usize>>,
w: u16,
h: u16,
struct SplashWindow {
frames: Vec<HBITMAP>,
rx: Receiver<i16>,
progress: i16,
frame_idx: usize,
w: i32,
h: i32,
hdc_screen: HDC,
}
fn average(numbers: &[u16]) -> u16 {
let sum: u16 = numbers.iter().sum();
let count = numbers.len() as u16;
fn average(numbers: &[u32]) -> u32 {
let sum: u32 = numbers.iter().sum();
let count = numbers.len() as u32;
sum / count
}
@@ -73,17 +71,30 @@ fn convert_rgba_to_bgra(image_data: &mut Vec<u8>) {
}
}
unsafe extern "system" fn window_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let ptr = GetWindowLongPtrW(hwnd, GWL_USERDATA) as *mut SplashWindow;
match ptr.as_mut() {
Some(data) => LRESULT(data.handle_event(hwnd, msg, wparam, lparam)),
// If the pointer is null, we can just call the default window procedure
None => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
fn rgb(red: u8, green: u8, blue: u8) -> COLORREF {
COLORREF((red as u32) | ((green as u32) << 8) | ((blue as u32) << 16))
}
impl SplashWindow {
pub fn new(app_name: String, img_stream: Vec<u8>, rx: Receiver<i16>) -> Result<Self> {
pub unsafe fn run(app_name: String, img_stream: Vec<u8>, rx: Receiver<i16>) -> Result<()> {
let mut delays = Vec::new();
let mut frames = Vec::new();
let fmt_cursor = Cursor::new(&img_stream);
let fmt_reader = ImageReader::new(fmt_cursor).with_guessed_format()?;
let fmt = fmt_reader.format();
let dims = &fmt_reader.into_dimensions()?;
let w: u16 = u16::try_from(dims.0)?;
let h: u16 = u16::try_from(dims.1)?;
let w: i32 = i32::try_from(dims.0)?;
let h: i32 = i32::try_from(dims.1)?;
if Some(ImageFormat::Gif) == fmt {
info!("Image is animated GIF ({}x{}), loading frames...", w, h);
@@ -93,22 +104,22 @@ impl SplashWindow {
for frame in dec_frames.into_iter() {
let frame = frame?;
let (num, dem) = frame.delay().numer_denom_ms();
delays.push((num / dem) as u16);
delays.push((num / dem) as u32);
let dynamic = DynamicImage::from(frame.buffer().to_owned());
let mut vec = dynamic.to_rgba8().to_vec();
convert_rgba_to_bgra(&mut vec);
let bitmap = w::HBITMAP::CreateBitmap(winsafe::SIZE { cx: w.into(), cy: h.into() }, 1, 32, vec.as_mut_ptr() as *mut u8)?;
let bitmap = CreateBitmap(w, h, 1, 32, Some(vec.as_mut_ptr() as *mut _));
frames.push(bitmap);
}
info!("Successfully loaded {} frames.", frames.len());
} else {
info!("Loading static image (detected {:?})...", fmt);
delays.push(16); // 60 fps
delays.push(16u32); // 60 fps
let img_cursor = Cursor::new(&img_stream);
let img_decoder = ImageReader::new(img_cursor).with_guessed_format()?.decode()?;
let mut vec = img_decoder.to_rgba8().to_vec();
convert_rgba_to_bgra(&mut vec);
let bitmap = w::HBITMAP::CreateBitmap(winsafe::SIZE { cx: w.into(), cy: h.into() }, 1, 32, vec.as_mut_ptr() as *mut u8)?;
let bitmap = CreateBitmap(w, h, 1, 32, Some(vec.as_mut_ptr() as *mut _));
frames.push(bitmap);
info!("Successfully loaded.");
}
@@ -117,256 +128,268 @@ impl SplashWindow {
// support a variable frame delay in the future.
let delay = average(&delays);
let wnd = gui::WindowMain::new(gui::WindowMainOpts {
class_icon: gui::Icon::Idi(co::IDI::APPLICATION),
class_cursor: gui::Cursor::Idc(co::IDC::APPSTARTING),
class_style: co::CS::HREDRAW | co::CS::VREDRAW,
class_name: "VelopackSetupSplashWindow".to_owned(),
title: app_name,
size: (w.into(), h.into()),
ex_style: co::WS_EX::NoValue,
style: co::WS::POPUP,
let class_name = string_to_wide("VelopackSetupSplashWindow");
let app_name = string_to_wide(&app_name);
let h_instance: HINSTANCE = GetModuleHandleW(None)?.into();
let wnd_class = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(window_proc),
hInstance: h_instance,
hCursor: LoadCursorW(None, IDC_APPSTARTING)?,
hbrBackground: CreateSolidBrush(rgb(0, 0, 0)),
lpszClassName: class_name.as_pcwstr(),
..Default::default()
});
};
let frames = Rc::new(frames);
let rx = Rc::new(rx);
let progress = Rc::new(RefCell::new(0));
let frame_idx = Rc::new(RefCell::new(0));
let mut new_self = Self { wnd, frames, delay, frame_idx, w, h, rx, progress };
new_self.events();
Ok(new_self)
}
let class_id = unsafe { RegisterClassExW(&wnd_class) };
if class_id == 0 {
// if class already registered we can ignore
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(1410) {
bail!("Failed to register window class: {:?}", err);
}
}
pub fn run(&self) -> Result<i32> {
let res = self.wnd.run_main(Some(co::SW::SHOWNOACTIVATE));
if res.is_err() {
error!("Error Showing Splash Window: {:?}", res);
bail!("Error Showing Splash Window: {:?}", res);
// center the window on the screen containing the cursor
let mut lppoint = POINT::default();
GetCursorPos(&mut lppoint)?;
let h_monitor = MonitorFromPoint(lppoint, MONITOR_DEFAULTTONEAREST);
let mut mi: MONITORINFO = Default::default();
mi.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
if GetMonitorInfoW(h_monitor, &mut mi).as_bool() {
// center the window in the monitor
let rc_monitor = mi.rcMonitor;
let left = (rc_monitor.left + rc_monitor.right - w) / 2;
let top = (rc_monitor.top + rc_monitor.bottom - h) / 2;
lppoint.x = left;
lppoint.y = top;
} else {
info!("Splash Window Closed");
}
Ok(res.unwrap())
}
fn events(&mut self) {
let self2 = self.clone();
self.wnd.on().wm_create(move |_m| {
// will ask Windows to give us a WM_TIMER every `delay` milliseconds
self2.wnd.hwnd().SetTimer(TMR_GIF, self2.delay.into(), None)?;
Ok(0)
});
self.wnd.on().wm_nc_hit_test(|_m| {
Ok(co::HT::CAPTION) // make the window draggable
});
let self2 = self.clone();
self.wnd.on().wm_timer(TMR_GIF, move || {
// handle any incoming messages before painting
loop {
let msg = self2.rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_NOMESSAGE {
break;
} else if msg == MSG_CLOSE {
self2.wnd.hwnd().SendMessage(w::msg::wm::Close {});
return Ok(());
} else if msg >= 0 {
let mut p = self2.progress.borrow_mut();
*p = msg;
}
}
// trigger a new WM_PAINT
self2.wnd.hwnd().InvalidateRect(None, false)?;
Ok(())
});
let self2 = self.clone();
self.wnd.on().wm_paint(move || {
// initial setup
let hwnd = self2.wnd.hwnd();
let rect = hwnd.GetClientRect()?;
let hdc = hwnd.BeginPaint()?;
let w = rect.right - rect.left;
let h = rect.bottom - rect.top;
let desktop = w::HWND::GetDesktopWindow();
let hdc_screen = desktop.GetDC()?;
// retrieve the next frame to draw
let mut idx = self2.frame_idx.borrow_mut();
let h_bitmap = self2.frames[*idx].deref();
*idx += 1;
if *idx >= self2.frames.len() {
*idx = 0;
}
// create double buffer
let hdc_mem = hdc_screen.CreateCompatibleDC()?;
let buffer_bmp = hdc_screen.CreateCompatibleBitmap(w, h)?;
let _buffer_old = hdc_mem.SelectObject(buffer_bmp.deref())?;
// load image into hdc_bitmap
let hdc_bitmap = hdc_screen.CreateCompatibleDC()?;
let _bitmap_old = hdc_bitmap.SelectObject(h_bitmap)?;
// draw background to hdc_mem
let background_brush = w::HBRUSH::CreateSolidBrush(w::COLORREF::new(0, 0, 0))?;
hdc_mem.FillRect(w::RECT { left: 0, top: 0, right: w, bottom: h }, &background_brush)?;
// copy bitmap from hdc_bitmap to hdc_mem
hdc_mem.SetStretchBltMode(co::STRETCH_MODE::STRETCH_HALFTONE)?;
hdc_mem.StretchBlt(
w::POINT { x: 0, y: 0 },
w::SIZE { cx: rect.right, cy: rect.bottom },
&hdc_bitmap,
w::POINT { x: 0, y: 0 },
w::SIZE { cx: self2.w.into(), cy: self2.h.into() },
co::ROP::SRCCOPY,
// fallback to work area if monitor info is not available
let mut rc_work_area: RECT = Default::default();
SystemParametersInfoW(
SPI_GETWORKAREA,
0,
Some(&mut rc_work_area as *mut RECT as *mut _),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
)?;
lppoint.x = (rc_work_area.left + rc_work_area.right - w) / 2;
lppoint.y = (rc_work_area.top + rc_work_area.bottom - h) / 2;
}
// draw progress bar to hdc_mem
let progress = self2.progress.borrow();
let progress_brush = w::HBRUSH::CreateSolidBrush(w::COLORREF::new(0, 255, 0))?;
let progress_width = (rect.right as f32 * (*progress as f32 / 100.0)) as i32;
let progress_rect = w::RECT { left: 0, bottom: rect.bottom, right: progress_width, top: rect.bottom - 10 };
hdc_mem.FillRect(progress_rect, &progress_brush)?;
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
class_name.as_pcwstr(),
app_name.as_pcwstr(),
WS_CLIPCHILDREN | WS_POPUP,
lppoint.x,
lppoint.y,
w,
h,
None,
None,
Some(h_instance),
None,
)?;
// finally, copy hdc_mem to hdc
hdc.BitBlt(w::POINT { x: 0, y: 0 }, w::SIZE { cx: w, cy: h }, &hdc_mem, w::POINT { x: 0, y: 0 }, co::ROP::SRCCOPY)?;
let desktop = unsafe { GetDesktopWindow() };
let hdc_screen = unsafe { GetDC(Some(desktop)) };
Ok(())
});
}
}
let data_ptr = Box::into_raw(Box::new(Self { frames, rx, frame_idx: 0, w, h, progress: 0, hdc_screen }));
pub const TDM_SET_PROGRESS_BAR_MARQUEE: co::WM = unsafe { co::WM::from_raw(1131) };
pub const TDM_SET_MARQUEE_PROGRESS_BAR: co::WM = unsafe { co::WM::from_raw(1127) };
pub const TDM_SET_PROGRESS_BAR_POS: co::WM = unsafe { co::WM::from_raw(1130) };
SetWindowLongPtrW(hwnd, GWL_USERDATA, data_ptr as isize);
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
SetTimer(Some(hwnd), TMR_GIF, delay, None);
struct MsgSetProgressMarqueeOnOff {
is_marquee_on: bool,
}
unsafe impl MsgSend for MsgSetProgressMarqueeOnOff {
type RetType = ();
fn convert_ret(&self, _: isize) -> Self::RetType {
()
}
fn as_generic_wm(&mut self) -> w::msg::WndMsg {
let v: usize = if self.is_marquee_on { 1 } else { 0 };
w::msg::WndMsg { msg_id: TDM_SET_PROGRESS_BAR_MARQUEE, wparam: v, lparam: 0 }
}
}
let mut msg = MSG::default();
let _ = PeekMessageW(&mut msg, Some(hwnd), 0, 0, PEEK_MESSAGE_REMOVE_TYPE(0)); // invoke creating message queue
struct MsgSetProgressMarqueeMode {
is_marquee_on: bool,
}
unsafe impl MsgSend for MsgSetProgressMarqueeMode {
type RetType = ();
fn convert_ret(&self, _: isize) -> Self::RetType {
()
}
fn as_generic_wm(&mut self) -> w::msg::WndMsg {
let v: usize = if self.is_marquee_on { 1 } else { 0 };
w::msg::WndMsg { msg_id: TDM_SET_MARQUEE_PROGRESS_BAR, wparam: v, lparam: 0 }
}
}
struct MsgSetProgressPos {
pos: usize,
}
unsafe impl MsgSend for MsgSetProgressPos {
type RetType = ();
fn convert_ret(&self, _: isize) -> Self::RetType {
()
}
fn as_generic_wm(&mut self) -> w::msg::WndMsg {
w::msg::WndMsg { msg_id: TDM_SET_PROGRESS_BAR_POS, wparam: self.pos, lparam: 0 }
}
}
#[derive(Clone)]
pub struct ComCtlProgressWindow {
// hwnd: Rc<RefCell<w::HWND>>,
rx: Rc<Receiver<i16>>,
last_progress: Rc<AtomicI16>,
}
impl ComCtlProgressWindow {
pub fn set_progress(&self, value: i16) {
self.last_progress.store(value, Ordering::SeqCst);
}
pub fn get_progress(&self) -> i16 {
self.last_progress.load(Ordering::SeqCst)
}
pub fn get_next_message(&self) -> i16 {
let mut progress: i16 = MSG_NOMESSAGE;
loop {
let msg = self.rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_NOMESSAGE {
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
if msg.message == WM_QUIT {
break;
} else {
progress = msg;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
let arc_data = Box::from_raw(data_ptr); // drop the reference to the data
let _ = DeleteDC(arc_data.hdc_screen);
for h_bitmap in &arc_data.frames {
let _ = DeleteObject((*h_bitmap).into());
}
let _ = DestroyWindow(hwnd);
Ok(())
}
pub unsafe fn handle_event(&mut self, hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> isize {
match msg {
WM_NCHITTEST => {
return HTCAPTION as isize; // make the window draggable
}
WM_TIMER => {
if wparam.0 as usize == TMR_GIF {
// handle any incoming messages before painting
let next_message = drain_and_get_next_message(&self.rx);
if next_message == MSG_CLOSE {
let _ = PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0));
return 0;
} else if next_message >= 0 {
self.progress = next_message;
}
// advance the frame index
self.frame_idx += 1;
if self.frame_idx >= self.frames.len() {
self.frame_idx = 0; // loop back to the first frame
}
// trigger a new WM_PAINT
let _ = InvalidateRect(Some(hwnd), None, false);
}
return 0;
}
WM_PAINT => {
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(hwnd, &mut ps);
if hdc.is_invalid() {
return 0;
}
// get the bitmap for the current frame
let h_bitmap = self.frames[self.frame_idx];
// create double buffer
let hdc_mem = CreateCompatibleDC(Some(self.hdc_screen));
let buffer_bmp = CreateCompatibleBitmap(self.hdc_screen, self.w, self.h);
let buffer_old = SelectObject(hdc_mem, buffer_bmp.into());
// load image into hdc_bitmap
let hdc_bitmap = CreateCompatibleDC(Some(self.hdc_screen));
let bitmap_old = SelectObject(hdc_bitmap, h_bitmap.into());
// draw background to hdc_mem
let background_brush = CreateSolidBrush(rgb(0, 0, 0));
FillRect(hdc_mem, &RECT { left: 0, top: 0, right: self.w, bottom: self.h }, background_brush);
// copy bitmap from hdc_bitmap to hdc_mem
SetStretchBltMode(hdc_mem, STRETCH_HALFTONE);
let _ = StretchBlt(hdc_mem, 0, 0, self.w, self.h, Some(hdc_bitmap), 0, 0, self.w, self.h, SRCCOPY);
// draw progress bar to hdc_mem
let progress = self.progress;
let progress_brush = CreateSolidBrush(rgb(15, 123, 15));
let progress_width = (self.w as f32 * (progress as f32 / 100.0)) as i32;
let progress_rect = RECT { left: 0, bottom: self.h, right: progress_width, top: self.h - 10 };
FillRect(hdc_mem, &progress_rect, progress_brush);
// finally, copy hdc_mem to hdc
let _ = BitBlt(hdc, 0, 0, self.w, self.h, Some(hdc_mem), 0, 0, SRCCOPY);
// clean up
let _ = DeleteObject(background_brush.into());
let _ = DeleteObject(progress_brush.into());
SelectObject(hdc_mem, buffer_old);
SelectObject(hdc_bitmap, bitmap_old);
let _ = DeleteDC(hdc_mem);
let _ = DeleteDC(hdc_bitmap);
let _ = DeleteObject(buffer_bmp.into());
let _ = EndPaint(hwnd, &ps);
return 0;
}
_ => {
// handle other messages
return DefWindowProcW(hwnd, msg, wparam, lparam).0;
}
}
progress
}
}
pub struct ComCtlProgressWindow {
rx: Receiver<i16>,
last_progress: i16,
}
fn show_com_ctl_progress_dialog(rx: Receiver<i16>, window_title: &str, content: &str) {
let mut window_title = WString::from_str(window_title);
let mut content = WString::from_str(content);
let window_title = string_to_wide(window_title);
let content = string_to_wide(content);
let ok_text = string_to_wide("Hide");
let mut ok_text_buf = WString::from_str("Hide");
let mut td_btn = w::TASKDIALOG_BUTTON::default();
td_btn.set_nButtonID(co::DLGID::OK.into());
td_btn.set_pszButtonText(Some(&mut ok_text_buf));
let mut custom_btns = Vec::with_capacity(1);
custom_btns.push(td_btn);
let td_btn = TASKDIALOG_BUTTON {
nButtonID: 1, // OK button id
pszButtonText: ok_text.as_pcwstr(),
};
let custom_btns = vec![td_btn];
let mut config: w::TASKDIALOGCONFIG = Default::default();
config.dwFlags = co::TDF::SIZE_TO_CONTENT | co::TDF::SHOW_PROGRESS_BAR | co::TDF::CALLBACK_TIMER;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
config.set_pszWindowTitle(Some(&mut window_title));
config.set_pszMainInstruction(Some(&mut content));
config.set_pButtons(Some(&mut custom_btns));
let mut config: TASKDIALOGCONFIG = Default::default();
config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
config.dwFlags = TDF_SIZE_TO_CONTENT | TDF_SHOW_PROGRESS_BAR | TDF_CALLBACK_TIMER;
config.pszWindowTitle = window_title.as_pcwstr();
config.pszMainInstruction = content.as_pcwstr();
config.pButtons = custom_btns.as_ptr();
config.cButtons = custom_btns.len() as u32;
config.nDefaultButton = 1;
config.Anonymous1.pszMainIcon = TD_INFORMATION_ICON;
// if (_icon != null) {
// config.dwFlags |= TASKDIALOG_FLAGS.TDF_USE_HICON_MAIN;
// config.mainIcon = _icon.Handle;
// }
let me = ComCtlProgressWindow { rx: Rc::new(rx), last_progress: Rc::new(AtomicI16::new(0)) };
config.lpCallbackData = &me as *const ComCtlProgressWindow as usize;
let me = ComCtlProgressWindow { rx, last_progress: 0 };
let data_ptr = Box::into_raw(Box::new(me));
config.lpCallbackData = data_ptr as isize;
config.pfCallback = Some(task_dialog_callback);
let _ = w::TaskDialogIndirect(&config, None);
unsafe {
let _ = TaskDialogIndirect(&config, None, None, None);
}
let _ = unsafe { Box::from_raw(data_ptr) }; // This will drop the ComCtlProgressWindow instance
}
extern "system" fn task_dialog_callback(hwnd: w::HWND, msg: co::TDN, _: usize, _: isize, lp_ref_data: usize) -> co::HRESULT {
let raw = lp_ref_data as *const ComCtlProgressWindow;
let me: &ComCtlProgressWindow = unsafe { &*raw };
fn drain_and_get_next_message(rx: &Receiver<i16>) -> i16 {
let mut progress: i16 = MSG_NOMESSAGE;
loop {
let msg = rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_NOMESSAGE {
break;
} else {
progress = msg;
}
}
progress
}
if msg == co::TDN::TIMER {
let next_message = me.get_next_message();
if next_message == MSG_CLOSE {
let _ = hwnd.EndDialog(0);
return co::HRESULT::S_OK;
} else if next_message == MSG_INDEFINITE {
hwnd.SendMessage(MsgSetProgressMarqueeOnOff { is_marquee_on: true });
hwnd.SendMessage(MsgSetProgressMarqueeMode { is_marquee_on: true });
me.set_progress(MSG_INDEFINITE);
} else if next_message >= 0 {
if me.get_progress() < 0 {
hwnd.SendMessage(MsgSetProgressMarqueeOnOff { is_marquee_on: false });
hwnd.SendMessage(MsgSetProgressMarqueeMode { is_marquee_on: false });
unsafe extern "system" fn task_dialog_callback(
hwnd: HWND,
msg: TASKDIALOG_NOTIFICATIONS,
_wparam: WPARAM,
_lparam: LPARAM,
lp_ref_data: isize,
) -> HRESULT {
let raw = lp_ref_data as *mut ComCtlProgressWindow;
if let Some(me) = raw.as_mut() {
if msg == TDN_TIMER {
let next_message = drain_and_get_next_message(&me.rx);
if next_message == MSG_CLOSE {
let _ = EndDialog(hwnd, 0);
} else if next_message == MSG_INDEFINITE {
SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_MARQUEE.0 as u32, Some(WPARAM(1)), Some(LPARAM(0)));
SendMessageW(hwnd, TDM_SET_MARQUEE_PROGRESS_BAR.0 as u32, Some(WPARAM(1)), Some(LPARAM(0)));
me.last_progress = MSG_INDEFINITE;
} else if next_message >= 0 {
if me.last_progress < 0 {
SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_MARQUEE.0 as u32, Some(WPARAM(0)), Some(LPARAM(0)));
SendMessageW(hwnd, TDM_SET_MARQUEE_PROGRESS_BAR.0 as u32, Some(WPARAM(0)), Some(LPARAM(0)));
}
SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_POS.0 as u32, Some(WPARAM(next_message as usize)), Some(LPARAM(0)));
me.last_progress = next_message;
}
hwnd.SendMessage(MsgSetProgressPos { pos: next_message as usize });
me.set_progress(next_message);
}
}
return co::HRESULT::S_OK;
return S_OK;
}
#[test]
@@ -374,8 +397,16 @@ extern "system" fn task_dialog_callback(hwnd: w::HWND, msg: co::TDN, _: usize, _
fn show_test_gif() {
let rd = std::fs::read(r"C:\Source\Clowd\artwork\splash.gif").unwrap();
let tx = show_splash_dialog("osu!".to_string(), Some(rd));
tx.send(80).unwrap();
std::thread::sleep(std::time::Duration::from_secs(6));
let _ = tx.send(25);
std::thread::sleep(std::time::Duration::from_secs(1));
let _ = tx.send(50);
std::thread::sleep(std::time::Duration::from_secs(1));
let _ = tx.send(75);
std::thread::sleep(std::time::Duration::from_secs(1));
let _ = tx.send(100);
std::thread::sleep(std::time::Duration::from_secs(3));
let _ = tx.send(MSG_CLOSE);
std::thread::sleep(std::time::Duration::from_secs(3));
}
#[test]

View File

@@ -1,34 +1,88 @@
use anyhow::Result;
use windows::core::{PCWSTR, PWSTR};
pub struct WideString {
inner: Vec<u16>,
}
impl WideString {
pub fn as_ptr(&self) -> *const u16 {
self.inner.as_ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut u16 {
self.inner.as_mut_ptr()
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn as_pcwstr(&self) -> PCWSTR {
PCWSTR(self.as_ptr())
}
pub fn as_pwstr(&mut self) -> PWSTR {
PWSTR(self.as_mut_ptr())
}
}
impl Into<Vec<u16>> for WideString {
fn into(self) -> Vec<u16> {
self.inner
}
}
impl Into<PCWSTR> for WideString {
fn into(self) -> PCWSTR {
self.as_pcwstr()
}
}
impl AsRef<[u16]> for WideString {
fn as_ref(&self) -> &[u16] {
&self.inner
}
}
impl AsMut<[u16]> for WideString {
fn as_mut(&mut self) -> &mut [u16] {
&mut self.inner
}
}
pub fn string_to_u16<P: AsRef<str>>(input: P) -> Vec<u16> {
let input = input.as_ref();
input.encode_utf16().chain(Some(0)).collect::<Vec<u16>>()
}
pub trait WideString {
pub fn string_to_wide<P: AsRef<str>>(input: P) -> WideString {
WideString { inner: string_to_u16(input) }
}
pub trait WideStringRef {
fn to_wide_slice(&self) -> &[u16];
}
impl WideString for PWSTR {
impl WideStringRef for PWSTR {
fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() }
}
}
impl WideString for PCWSTR {
impl WideStringRef for PCWSTR {
fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() }
}
}
impl WideString for Vec<u16> {
impl WideStringRef for Vec<u16> {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
impl WideString for &Vec<u16> {
impl WideStringRef for &Vec<u16> {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
@@ -40,26 +94,27 @@ impl WideString for &Vec<u16> {
// }
// }
impl<const N: usize> WideString for [u16; N] {
impl<const N: usize> WideStringRef for [u16; N] {
fn to_wide_slice(&self) -> &[u16] {
self.as_ref()
}
}
pub fn u16_to_string_lossy<T: WideString>(input: T) -> String {
pub fn u16_to_string_lossy<T: WideStringRef>(input: T) -> String {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
String::from_utf16_lossy(trimmed_slice)
}
pub fn u16_to_string<T: WideString>(input: T) -> Result<String> {
pub fn u16_to_string<T: WideStringRef>(input: T) -> Result<String> {
let slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos];
Ok(String::from_utf16(trimmed_slice)?)
}
// pub fn pwstr_to_string(input: PWSTR) -> Result<String> {
// unsafe {
// let hstring = input.to_hstring();

View File

@@ -5,16 +5,16 @@ use common::*;
use std::hint::assert_unchecked;
use std::{fs, path::Path, path::PathBuf};
use tempfile::tempdir;
use velopack_bins::*;
use velopack_bins::*;
use velopack_bins::windows::known_path;
use velopack::bundle::load_bundle_from_file;
use velopack::locator::{auto_locate_app_manifest, LocationContext};
#[cfg(target_os = "windows")]
use winsafe::{self as w, co};
#[cfg(target_os = "windows")]
#[test]
pub fn test_install_apply_uninstall() {
dialogs::set_silent(true);
let fixtures = find_fixtures();
@@ -22,10 +22,8 @@ pub fn test_install_apply_uninstall() {
let app_id = "AvaloniaCrossPlat";
let pkg_name = "AvaloniaCrossPlat-1.0.11-win-full.nupkg";
let start_menu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None).unwrap();
let start_menu = Path::new(&start_menu).join("Programs");
let desktop = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::Desktop, co::KF::DONT_UNEXPAND, None).unwrap();
let desktop = Path::new(&desktop);
let start_menu = PathBuf::from(known_path::get_start_menu().unwrap());
let desktop = PathBuf::from(known_path::get_user_desktop().unwrap());
let lnk_start_1 = start_menu.join(format!("{}.lnk", app_id));
let lnk_desktop_1 = desktop.join(format!("{}.lnk", app_id));