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", "groupName": "frozen",
"matchPackageNames": [ "matchPackageNames": [
"winsafe", // newer versions causes runtime errors in release builds
"System.CommandLine", // too many breaking changes too frequently "System.CommandLine", // too many breaking changes too frequently
"xunit.runner.visualstudio", // 20-12-2024: broke tests (something about sn signing maybe?) "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 "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", "walkdir",
"webview2-com-sys", "webview2-com-sys",
"windows", "windows",
"winreg",
"winres", "winres",
"winsafe",
"zip", "zip",
"zstd", "zstd",
] ]
@@ -2844,6 +2844,16 @@ dependencies = [
"memchr", "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]] [[package]]
name = "winres" name = "winres"
version = "0.1.12" version = "0.1.12"
@@ -2853,12 +2863,6 @@ dependencies = [
"toml 0.5.11", "toml 0.5.11",
] ]
[[package]]
name = "winsafe"
version = "0.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40369220be405a294b88b13ccc3d916fac12423250b30092c8c4ea19001f7f1"
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"
version = "0.39.0" version = "0.39.0"

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,22 @@
use super::{dialogs_common::*, dialogs_const::*}; use super::{dialogs_common::*, dialogs_const::*};
use velopack::bundle::Manifest; use crate::windows::strings::string_to_wide;
use anyhow::Result; use anyhow::Result;
use std::path::PathBuf; use std::path::PathBuf;
use winsafe::{self as w, co, prelude::*, WString}; use velopack::{
use velopack::locator::{auto_locate_app_manifest, LocationContext}; 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) { pub fn show_restart_required(app: &Manifest) {
show_warn( show_warn(
@@ -58,32 +71,33 @@ pub fn show_uninstall_complete_with_errors_dialog(app_title: &str, log_path: Opt
return; return;
} }
let mut setup_name = WString::from_str(format!("{} Uninstall", app_title)); let setup_name = string_to_wide(format!("{} Uninstall", app_title));
let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app_title)); let instruction = string_to_wide(format!("{} uninstall has completed with errors.", app_title));
let mut content = WString::from_str( 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.", "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(); let mut config = TASKDIALOGCONFIG::default();
config.dwFlags = co::TDF::ENABLE_HYPERLINKS | co::TDF::SIZE_TO_CONTENT; config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
config.dwCommonButtons = co::TDCBF::OK; config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_SIZE_TO_CONTENT;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::WARNING)); config.dwCommonButtons = TDCBF_OK_BUTTON;
config.set_pszWindowTitle(Some(&mut setup_name)); config.pszWindowTitle = setup_name.as_pcwstr();
config.set_pszMainInstruction(Some(&mut instruction)); config.pszMainInstruction = instruction.as_pcwstr();
config.set_pszContent(Some(&mut content)); 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 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 let Some(log_path) = log_path {
if log_path.exists() { if log_path.exists() {
config.set_pszFooterIcon(w::IconId::Id(co::TD_ICON::INFORMATION.into())); config.Anonymous2.pszFooterIcon = TD_INFORMATION_ICON;
config.set_pszFooter(Some(&mut footer)); config.pszFooter = footer.as_pcwstr();
config.lpCallbackData = log_path as *const PathBuf as usize; config.lpCallbackData = log_path as *const PathBuf as isize;
config.pfCallback = Some(task_dialog_callback); 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 { 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; return DialogResult::Cancel;
} }
let mut config: w::TASKDIALOGCONFIG = Default::default(); let mut config = TASKDIALOGCONFIG::default();
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION)); 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 update_name = string_to_wide(format!("{} Update {}", app_title, app_version));
let mut instruction = WString::from_str(format!("{} Update", app_title)); let instruction = string_to_wide(format!("{} Update", app_title));
let content = string_to_wide(format!(
let mut content = WString::from_str(format!(
"There are programs ({}) preventing the {} update from proceeding. \n\n\ "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.", 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)); process_names, app_title));
let mut btn_retry_txt = WString::from_str("Retry\nTry again if you've closed the program(s)"); let btn_retry_txt = string_to_wide("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 btn_continue_txt = string_to_wide("Continue\nAttempt to close the program(s) automatically");
let mut btn_cancel_txt = WString::from_str("Cancel\nThe update will not continue"); 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(); config.dwFlags = TDF_USE_COMMAND_LINKS;
btn_retry.set_nButtonID(co::DLGID::RETRY.into()); config.cButtons = custom_btns.len() as u32;
btn_retry.set_pszButtonText(Some(&mut btn_retry_txt)); 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(); let mut pnbutton = 0;
btn_continue.set_nButtonID(co::DLGID::CONTINUE.into()); let mut pnradiobutton = 0;
btn_continue.set_pszButtonText(Some(&mut btn_continue_txt)); let mut pfverificationflagchecked = FALSE;
let mut btn_cancel = w::TASKDIALOG_BUTTON::default(); unsafe { TaskDialogIndirect(&config, Some(&mut pnbutton), Some(&mut pnradiobutton), Some(&mut pfverificationflagchecked)).ok() };
btn_cancel.set_nButtonID(co::DLGID::CANCEL.into()); DialogResult::from_win(pnbutton)
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)
} }
pub fn show_overwrite_repair_dialog(app: &Manifest, root_path: &PathBuf, root_is_default: bool) -> bool { 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; return true;
} }
// these are the defaults, if we can't detect the current app version - we call it "Repair" let mut config = TASKDIALOGCONFIG::default();
let mut config: w::TASKDIALOGCONFIG = Default::default(); config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::WARNING)); config.Anonymous1.pszMainIcon = TD_WARNING_ICON;
let mut setup_name = WString::from_str(format!("{} Setup {}", app.title, app.version)); let setup_name = string_to_wide(format!("{} Setup {}", app.title, app.version));
let mut instruction = WString::from_str(format!("{} is already installed.", app.title)); let mut instruction = string_to_wide(format!("{} is already installed.", app.title));
let mut content = WString::from_str( let mut content =
"This application is installed on your computer. If it is not functioning correctly, you can attempt to repair it.", 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 mut btn_yes_txt = WString::from_str(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");
let mut btn_cancel_txt = WString::from_str("Cancel\nBackup or save your work first");
// if we can detect the current app version, we call it "Update" or "Downgrade" // 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())); let old_app = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(root_path.to_owned()));
if let Ok(old) = old_app { if let Ok(old) = old_app {
let old_version = old.get_manifest_version(); let old_version = old.get_manifest_version();
if old_version < app.version { if old_version < app.version {
instruction = WString::from_str(format!("An older version of {} is installed.", app.title)); instruction = string_to_wide(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)); content = string_to_wide(format!("Would you like to update from {} to {}?", old_version, app.version));
btn_yes_txt = WString::from_str(format!("Update\nTo version {}", app.version)); btn_yes_txt = string_to_wide(format!("Update\nTo version {}", app.version));
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION)); config.Anonymous1.pszMainIcon = TD_INFORMATION_ICON;
} else if old_version > app.version { } else if old_version > app.version {
instruction = WString::from_str(format!("A newer version of {} is installed.", app.title)); instruction = string_to_wide(format!("A newer version of {} is installed.", app.title));
content = WString::from_str(format!( content = string_to_wide(format!(
"You already have {} installed. Would you like to downgrade this application to an older version?", "You already have {} installed. Would you like to downgrade this application to an older version?",
old_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 { let footer = if root_is_default {
WString::from_str(format!("The install directory is '<A HREF=\"na\">%LocalAppData%\\{}</A>'", app.id)) string_to_wide(format!("The install directory is '<A HREF=\"na\">%LocalAppData%\\{}</A>'", app.id))
} else { } 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(); let btn_yes = TASKDIALOG_BUTTON { nButtonID: IDYES.0, pszButtonText: btn_yes_txt.as_pcwstr() };
btn_yes.set_nButtonID(co::DLGID::YES.into()); let btn_cancel = TASKDIALOG_BUTTON { nButtonID: IDCANCEL.0, pszButtonText: btn_cancel_txt.as_pcwstr() };
btn_yes.set_pszButtonText(Some(&mut btn_yes_txt)); let custom_btns = vec![btn_yes, btn_cancel];
let mut btn_cancel = w::TASKDIALOG_BUTTON::default(); config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_USE_COMMAND_LINKS;
btn_cancel.set_nButtonID(co::DLGID::CANCEL.into()); config.cButtons = custom_btns.len() as u32;
btn_cancel.set_pszButtonText(Some(&mut btn_cancel_txt)); 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); config.lpCallbackData = root_path as *const PathBuf as isize;
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.pfCallback = Some(task_dialog_callback); config.pfCallback = Some(task_dialog_callback);
let (btn, _) = w::TaskDialogIndirect(&config, None).ok().unwrap_or_else(|| (co::DLGID::YES, 0)); let mut pnbutton = 0;
return btn == co::DLGID::YES; 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 { extern "system" fn task_dialog_callback(_hwnd: HWND, msg: TASKDIALOG_NOTIFICATIONS, _: WPARAM, _: LPARAM, lp_ref_data: isize) -> HRESULT {
if msg == co::TDN::HYPERLINK_CLICKED { if msg == TDN_HYPERLINK_CLICKED {
let raw = lp_ref_data as *const PathBuf; let raw = lp_ref_data as *const PathBuf;
let path: &PathBuf = unsafe { &*raw }; let path: &PathBuf = unsafe { &*raw };
let dir = path.to_str().unwrap(); let dir = path.to_string_lossy().to_string();
w::HWND::GetDesktopWindow().ShellExecute("open", &dir, None, None, co::SW::SHOWDEFAULT).ok(); let dir = string_to_wide(dir);
return co::HRESULT::S_FALSE; // do not close dialog 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( pub fn generate_confirm(
@@ -217,42 +223,40 @@ pub fn generate_confirm(
btns: DialogButton, btns: DialogButton,
ico: DialogIcon, ico: DialogIcon,
) -> Result<DialogResult> { ) -> Result<DialogResult> {
let hparent = w::HWND::GetDesktopWindow(); let hparent = unsafe { GetDesktopWindow() };
let mut ok_text_buf = WString::from_opt_str(ok_text); let mut ok_text_buf = ok_text.map(string_to_wide);
let mut custom_btns = if ok_text.is_some() { let mut custom_btns = if let Some(ok_text_buf) = ok_text_buf.as_mut() {
let mut td_btn = w::TASKDIALOG_BUTTON::default(); let td_btn = TASKDIALOG_BUTTON { nButtonID: IDOK.0, pszButtonText: ok_text_buf.as_pcwstr() };
td_btn.set_nButtonID(co::DLGID::OK.into()); vec![td_btn]
td_btn.set_pszButtonText(Some(&mut ok_text_buf));
let mut custom_btns = Vec::with_capacity(1);
custom_btns.push(td_btn);
custom_btns
} else { } else {
Vec::<w::TASKDIALOG_BUTTON>::default() Vec::new()
}; };
let mut tdc = w::TASKDIALOGCONFIG::default(); let mut tdc = TASKDIALOGCONFIG { hwndParent: hparent, ..Default::default() };
tdc.hwndParent = unsafe { hparent.raw_copy() }; tdc.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as u32;
tdc.dwFlags = co::TDF::ALLOW_DIALOG_CANCELLATION | co::TDF::POSITION_RELATIVE_TO_WINDOW; tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_POSITION_RELATIVE_TO_WINDOW;
tdc.dwCommonButtons = btns.to_win(); 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() { if !custom_btns.is_empty() {
tdc.set_pButtons(Some(&mut custom_btns)); tdc.cButtons = custom_btns.len() as u32;
tdc.pButtons = custom_btns.as_mut_ptr();
} }
let mut title_buf = WString::from_str(title); let title_buf = string_to_wide(title);
tdc.set_pszWindowTitle(Some(&mut title_buf)); tdc.pszWindowTitle = title_buf.as_pcwstr();
let mut header_buf = WString::from_opt_str(header); let mut header_buf = header.map(string_to_wide);
if header.is_some() { if let Some(header_buf) = header_buf.as_mut() {
tdc.set_pszMainInstruction(Some(&mut header_buf)); tdc.pszMainInstruction = header_buf.as_pcwstr();
} }
let mut body_buf = WString::from_str(body); let body_buf = string_to_wide(body);
tdc.set_pszContent(Some(&mut body_buf)); tdc.pszContent = body_buf.as_pcwstr();
let result = w::TaskDialogIndirect(&tdc, None).map(|(dlg_id, _)| dlg_id)?; let mut pnbutton = 0;
Ok(DialogResult::from_win(result)) unsafe { TaskDialogIndirect(&tdc, Some(&mut pnbutton), None, None).expect("didnt work") };
Ok(DialogResult::from_win(pnbutton))
} }
pub fn generate_alert( pub fn generate_alert(
@@ -263,6 +267,28 @@ pub fn generate_alert(
btns: DialogButton, btns: DialogButton,
ico: DialogIcon, ico: DialogIcon,
) -> Result<()> { ) -> Result<()> {
let _ = generate_confirm(title, header, body, ok_text, btns, ico).map(|_| ())?; let _ = generate_confirm(title, header, body, ok_text, btns, ico)?;
Ok(()) 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 runtimes;
pub mod splash; pub mod splash;
pub mod known_path; pub mod known_path;
pub mod strings; pub mod strings;
pub mod registry; pub mod registry;
pub mod webview2; pub mod webview2;

View File

@@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use chrono::{Datelike, Local as DateTime}; use chrono::{Datelike, Local as DateTime};
use velopack::locator::VelopackLocator; 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"; 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 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 = 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 short_version = locator.get_manifest_version_short_string();
let now = DateTime::now(); 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_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet: String = format!("\"{}\" --uninstall --silent", updater_path); let uninstall_quiet: String = format!("\"{}\" --uninstall --silent", updater_path);
let reg_uninstall = let hkcu = RegKey::predef(HKEY_CURRENT_USER);
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0; let (reg_uninstall, _reg_uninstall_disp) = hkcu.create_subkey(UNINSTALL_REGISTRY_KEY)?;
let reg_app = reg_uninstall.RegCreateKeyEx(&app_id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0; let (reg_app, _reg_app_disp) = reg_uninstall.create_subkey(&app_id)?;
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(app_title))?; let u32true = 1u32;
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(short_version))?; let language = 0x0409u32;
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.set_value("DisplayIcon", &main_exe_path)?;
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(app_authors))?; reg_app.set_value("DisplayName", &app_title)?;
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?; reg_app.set_value("DisplayVersion", &short_version)?;
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?; reg_app.set_value("InstallDate", &formatted_date)?;
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?; reg_app.set_value("InstallLocation", &root_path_str)?;
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?; reg_app.set_value("Publisher", &app_authors)?;
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?; reg_app.set_value("QuietUninstallString", &uninstall_quiet)?;
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?; 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(()) Ok(())
} }
pub fn remove_uninstall_entry(locator: &VelopackLocator) -> Result<()> { pub fn remove_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
info!("Removing uninstall registry keys..."); info!("Removing uninstall registry keys...");
let app_id = locator.get_manifest_id(); let app_id = locator.get_manifest_id();
let reg_uninstall = let hkcu = RegKey::predef(HKEY_CURRENT_USER);
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0; let (reg_uninstall, _reg_uninstall_disp) = hkcu.create_subkey(UNINSTALL_REGISTRY_KEY)?;
reg_uninstall.RegDeleteKey(&app_id)?; reg_uninstall.delete_subkey_all(&app_id)?;
Ok(()) Ok(())
} }

View File

@@ -4,7 +4,7 @@ use regex::Regex;
use std::process::Command as Process; use std::process::Command as Process;
use std::{collections::HashMap, fs, path::Path}; use std::{collections::HashMap, fs, path::Path};
use velopack::download; 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_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"; 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 { fn is_installed(&self) -> bool {
let lm = w::HKEY::LOCAL_MACHINE; let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = lm.RegOpenKeyEx(Some(NDP_REG_KEY), co::REG_OPTION::NoValue, co::KEY::READ); match hklm.open_subkey(NDP_REG_KEY) {
if key.is_err() { Ok(ndp) => {
// key doesn't exist, so .net framework not installed let dword_val: std::io::Result<u32> = ndp.get_value("Release");
return false; if let Ok(rel) = dword_val {
rel >= self.release_version
} else {
false // key doesn't exist, so .net framework not installed
} }
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() { Err(_) => false, // key doesn't exist, so .net framework not installed
w::RegistryValue::Dword(v) => return v >= self.release_version,
_ => return false,
} }
} }
@@ -186,8 +183,8 @@ impl RuntimeInfo for VCRedistInfo {
fn is_installed(&self) -> bool { fn is_installed(&self) -> bool {
let mut installed_programs = HashMap::new(); 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, KEY_READ | 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_64KEY);
let (my_major, my_minor, my_build, _) = util::parse_version(&self.min_version).unwrap(); 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(); let reg = Regex::new(r"(?i)Microsoft Visual C\+\+(.*)Redistributable").unwrap();
for (k, v) in installed_programs { 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) { fn get_installed_programs(map: &mut HashMap<String, String>, access_rights: u32) {
let key = w::HKEY::LOCAL_MACHINE.RegOpenKeyEx(Some(UNINSTALL_REG_KEY), co::REG_OPTION::NoValue, access_rights); 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(view) = key {
if let Ok(iter) = view.RegEnumKeyEx() { for key_result in view.enum_keys() {
for key_result in iter {
if let Ok(key_name) = key_result { if let Ok(key_name) = key_result {
let subkey = view.RegOpenKeyEx(Some(&key_name), co::REG_OPTION::NoValue, access_rights); let subkey = view.open_subkey_with_flags(key_name, access_rights);
if subkey.is_err() { if subkey.is_err() {
continue; continue;
} }
let subkey = subkey.unwrap(); let subkey = subkey.unwrap();
let name = subkey.RegQueryValueEx(Some("DisplayName")); let name: std::io::Result<String> = subkey.get_value("DisplayName");
let version = subkey.RegQueryValueEx(Some("DisplayVersion")); let version: std::io::Result<String> = subkey.get_value("DisplayVersion");
if name.is_ok() && version.is_ok() { if name.is_ok() && version.is_ok() {
if let w::RegistryValue::Sz(display_name) = name.unwrap() { map.insert(name.unwrap(), version.unwrap());
if let w::RegistryValue::Sz(display_version) = version.unwrap() {
map.insert(display_name, display_version);
}
}
}
} }
} }
} }
@@ -241,7 +234,7 @@ fn get_installed_programs(map: &mut HashMap<String, String>, access_rights: co::
#[test] #[test]
fn test_get_installed_programs_returns_visual_studio() { fn test_get_installed_programs_returns_visual_studio() {
let mut map = HashMap::new(); 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")); 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> { 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); info!("Running installer: '{}', args={:?}", installer_path, args);
let mut cmd = Process::new(installer_path).args(&args).spawn()?; 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 anyhow::{bail, Result};
use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, ImageFormat, ImageReader}; use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, ImageFormat, ImageReader};
use std::sync::atomic::{AtomicI16, Ordering}; use std::sync::mpsc::{self, Receiver, Sender};
use std::{ use std::{io::Cursor, thread};
cell::RefCell, use windows::{
io::Cursor, core::HRESULT,
ops::Deref, Win32::{
rc::Rc, Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, S_OK, WPARAM},
sync::mpsc::{self, Receiver, Sender}, Graphics::Gdi::*,
thread, 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 TMR_GIF: usize = 1;
const MSG_NOMESSAGE: i16 = -99; 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 || { thread::spawn(move || {
info!("Showing splash screen immediately..."); info!("Showing splash screen immediately...");
if imgstream.is_some() { if imgstream.is_some() {
let _ = SplashWindow::new(app_name, imgstream.unwrap(), rx).and_then(|w| { if let Err(e) = unsafe { SplashWindow::run(app_name, imgstream.unwrap(), rx) } {
w.run()?; error!("Failed to show splash screen: {:?}", e);
Ok(()) }
});
} else { } else {
let setup_name = format!("{} Setup", app_name); let setup_name = format!("{} Setup", app_name);
let content = format!("Installing {}...", 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 tx
} }
#[derive(Clone)] struct SplashWindow {
pub struct SplashWindow { frames: Vec<HBITMAP>,
wnd: gui::WindowMain, rx: Receiver<i16>,
frames: Rc<Vec<DeleteObjectGuard<w::HBITMAP>>>, progress: i16,
rx: Rc<Receiver<i16>>, frame_idx: usize,
delay: u16, w: i32,
progress: Rc<RefCell<i16>>, h: i32,
frame_idx: Rc<RefCell<usize>>, hdc_screen: HDC,
w: u16,
h: u16,
} }
fn average(numbers: &[u16]) -> u16 { fn average(numbers: &[u32]) -> u32 {
let sum: u16 = numbers.iter().sum(); let sum: u32 = numbers.iter().sum();
let count = numbers.len() as u16; let count = numbers.len() as u32;
sum / count 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 { 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 delays = Vec::new();
let mut frames = Vec::new(); let mut frames = Vec::new();
let fmt_cursor = Cursor::new(&img_stream); let fmt_cursor = Cursor::new(&img_stream);
let fmt_reader = ImageReader::new(fmt_cursor).with_guessed_format()?; let fmt_reader = ImageReader::new(fmt_cursor).with_guessed_format()?;
let fmt = fmt_reader.format(); let fmt = fmt_reader.format();
let dims = &fmt_reader.into_dimensions()?; let dims = &fmt_reader.into_dimensions()?;
let w: u16 = u16::try_from(dims.0)?; let w: i32 = i32::try_from(dims.0)?;
let h: u16 = u16::try_from(dims.1)?; let h: i32 = i32::try_from(dims.1)?;
if Some(ImageFormat::Gif) == fmt { if Some(ImageFormat::Gif) == fmt {
info!("Image is animated GIF ({}x{}), loading frames...", w, h); info!("Image is animated GIF ({}x{}), loading frames...", w, h);
@@ -93,22 +104,22 @@ impl SplashWindow {
for frame in dec_frames.into_iter() { for frame in dec_frames.into_iter() {
let frame = frame?; let frame = frame?;
let (num, dem) = frame.delay().numer_denom_ms(); 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 dynamic = DynamicImage::from(frame.buffer().to_owned());
let mut vec = dynamic.to_rgba8().to_vec(); let mut vec = dynamic.to_rgba8().to_vec();
convert_rgba_to_bgra(&mut 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); frames.push(bitmap);
} }
info!("Successfully loaded {} frames.", frames.len()); info!("Successfully loaded {} frames.", frames.len());
} else { } else {
info!("Loading static image (detected {:?})...", fmt); 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_cursor = Cursor::new(&img_stream);
let img_decoder = ImageReader::new(img_cursor).with_guessed_format()?.decode()?; let img_decoder = ImageReader::new(img_cursor).with_guessed_format()?.decode()?;
let mut vec = img_decoder.to_rgba8().to_vec(); let mut vec = img_decoder.to_rgba8().to_vec();
convert_rgba_to_bgra(&mut 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); frames.push(bitmap);
info!("Successfully loaded."); info!("Successfully loaded.");
} }
@@ -117,192 +128,229 @@ impl SplashWindow {
// support a variable frame delay in the future. // support a variable frame delay in the future.
let delay = average(&delays); let delay = average(&delays);
let wnd = gui::WindowMain::new(gui::WindowMainOpts { let class_name = string_to_wide("VelopackSetupSplashWindow");
class_icon: gui::Icon::Idi(co::IDI::APPLICATION), let app_name = string_to_wide(&app_name);
class_cursor: gui::Cursor::Idc(co::IDC::APPSTARTING),
class_style: co::CS::HREDRAW | co::CS::VREDRAW, let h_instance: HINSTANCE = GetModuleHandleW(None)?.into();
class_name: "VelopackSetupSplashWindow".to_owned(),
title: app_name, let wnd_class = WNDCLASSEXW {
size: (w.into(), h.into()), cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
ex_style: co::WS_EX::NoValue, style: CS_HREDRAW | CS_VREDRAW,
style: co::WS::POPUP, 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() ..Default::default()
}); };
let frames = Rc::new(frames); let class_id = unsafe { RegisterClassExW(&wnd_class) };
let rx = Rc::new(rx); if class_id == 0 {
let progress = Rc::new(RefCell::new(0)); // if class already registered we can ignore
let frame_idx = Rc::new(RefCell::new(0)); let err = std::io::Error::last_os_error();
let mut new_self = Self { wnd, frames, delay, frame_idx, w, h, rx, progress }; if err.raw_os_error() != Some(1410) {
new_self.events(); bail!("Failed to register window class: {:?}", err);
Ok(new_self) }
} }
pub fn run(&self) -> Result<i32> { // center the window on the screen containing the cursor
let res = self.wnd.run_main(Some(co::SW::SHOWNOACTIVATE)); let mut lppoint = POINT::default();
if res.is_err() { GetCursorPos(&mut lppoint)?;
error!("Error Showing Splash Window: {:?}", res);
bail!("Error Showing Splash Window: {:?}", res); 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 { } else {
info!("Splash Window Closed"); // fallback to work area if monitor info is not available
} let mut rc_work_area: RECT = Default::default();
Ok(res.unwrap()) 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;
} }
fn events(&mut self) { let hwnd = CreateWindowExW(
let self2 = self.clone(); WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
self.wnd.on().wm_create(move |_m| { class_name.as_pcwstr(),
// will ask Windows to give us a WM_TIMER every `delay` milliseconds app_name.as_pcwstr(),
self2.wnd.hwnd().SetTimer(TMR_GIF, self2.delay.into(), None)?; WS_CLIPCHILDREN | WS_POPUP,
Ok(0) lppoint.x,
}); lppoint.y,
w,
h,
None,
None,
Some(h_instance),
None,
)?;
self.wnd.on().wm_nc_hit_test(|_m| { let desktop = unsafe { GetDesktopWindow() };
Ok(co::HT::CAPTION) // make the window draggable let hdc_screen = unsafe { GetDC(Some(desktop)) };
});
let self2 = self.clone(); let data_ptr = Box::into_raw(Box::new(Self { frames, rx, frame_idx: 0, w, h, progress: 0, hdc_screen }));
self.wnd.on().wm_timer(TMR_GIF, move || {
// handle any incoming messages before painting SetWindowLongPtrW(hwnd, GWL_USERDATA, data_ptr as isize);
loop { let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
let msg = self2.rx.try_recv().unwrap_or(MSG_NOMESSAGE); SetTimer(Some(hwnd), TMR_GIF, delay, None);
if msg == MSG_NOMESSAGE {
let mut msg = MSG::default();
let _ = PeekMessageW(&mut msg, Some(hwnd), 0, 0, PEEK_MESSAGE_REMOVE_TYPE(0)); // invoke creating message queue
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
if msg.message == WM_QUIT {
break; 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;
} }
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 // trigger a new WM_PAINT
self2.wnd.hwnd().InvalidateRect(None, false)?; let _ = InvalidateRect(Some(hwnd), 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;
} }
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 // create double buffer
let hdc_mem = hdc_screen.CreateCompatibleDC()?; let hdc_mem = CreateCompatibleDC(Some(self.hdc_screen));
let buffer_bmp = hdc_screen.CreateCompatibleBitmap(w, h)?; let buffer_bmp = CreateCompatibleBitmap(self.hdc_screen, self.w, self.h);
let _buffer_old = hdc_mem.SelectObject(buffer_bmp.deref())?; let buffer_old = SelectObject(hdc_mem, buffer_bmp.into());
// load image into hdc_bitmap // load image into hdc_bitmap
let hdc_bitmap = hdc_screen.CreateCompatibleDC()?; let hdc_bitmap = CreateCompatibleDC(Some(self.hdc_screen));
let _bitmap_old = hdc_bitmap.SelectObject(h_bitmap)?; let bitmap_old = SelectObject(hdc_bitmap, h_bitmap.into());
// draw background to hdc_mem // draw background to hdc_mem
let background_brush = w::HBRUSH::CreateSolidBrush(w::COLORREF::new(0, 0, 0))?; let background_brush = CreateSolidBrush(rgb(0, 0, 0));
hdc_mem.FillRect(w::RECT { left: 0, top: 0, right: w, bottom: h }, &background_brush)?; FillRect(hdc_mem, &RECT { left: 0, top: 0, right: self.w, bottom: self.h }, background_brush);
// copy bitmap from hdc_bitmap to hdc_mem // copy bitmap from hdc_bitmap to hdc_mem
hdc_mem.SetStretchBltMode(co::STRETCH_MODE::STRETCH_HALFTONE)?; SetStretchBltMode(hdc_mem, STRETCH_HALFTONE);
hdc_mem.StretchBlt( let _ = StretchBlt(hdc_mem, 0, 0, self.w, self.h, Some(hdc_bitmap), 0, 0, self.w, self.h, SRCCOPY);
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,
)?;
// draw progress bar to hdc_mem // draw progress bar to hdc_mem
let progress = self2.progress.borrow(); let progress = self.progress;
let progress_brush = w::HBRUSH::CreateSolidBrush(w::COLORREF::new(0, 255, 0))?; let progress_brush = CreateSolidBrush(rgb(15, 123, 15));
let progress_width = (rect.right as f32 * (*progress as f32 / 100.0)) as i32; let progress_width = (self.w 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 }; let progress_rect = RECT { left: 0, bottom: self.h, right: progress_width, top: self.h - 10 };
hdc_mem.FillRect(progress_rect, &progress_brush)?; FillRect(hdc_mem, &progress_rect, progress_brush);
// finally, copy hdc_mem to hdc // 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 _ = BitBlt(hdc, 0, 0, self.w, self.h, Some(hdc_mem), 0, 0, SRCCOPY);
Ok(()) // 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;
}
}
} }
} }
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) };
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 }
}
}
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 { pub struct ComCtlProgressWindow {
// hwnd: Rc<RefCell<w::HWND>>, rx: Receiver<i16>,
rx: Rc<Receiver<i16>>, last_progress: i16,
last_progress: Rc<AtomicI16>,
} }
impl ComCtlProgressWindow { fn show_com_ctl_progress_dialog(rx: Receiver<i16>, window_title: &str, content: &str) {
pub fn set_progress(&self, value: i16) { let window_title = string_to_wide(window_title);
self.last_progress.store(value, Ordering::SeqCst); let content = string_to_wide(content);
let ok_text = string_to_wide("Hide");
let td_btn = TASKDIALOG_BUTTON {
nButtonID: 1, // OK button id
pszButtonText: ok_text.as_pcwstr(),
};
let custom_btns = vec![td_btn];
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;
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);
unsafe {
let _ = TaskDialogIndirect(&config, None, None, None);
} }
pub fn get_progress(&self) -> i16 {
self.last_progress.load(Ordering::SeqCst) let _ = unsafe { Box::from_raw(data_ptr) }; // This will drop the ComCtlProgressWindow instance
} }
pub fn get_next_message(&self) -> i16 {
fn drain_and_get_next_message(rx: &Receiver<i16>) -> i16 {
let mut progress: i16 = MSG_NOMESSAGE; let mut progress: i16 = MSG_NOMESSAGE;
loop { loop {
let msg = self.rx.try_recv().unwrap_or(MSG_NOMESSAGE); let msg = rx.try_recv().unwrap_or(MSG_NOMESSAGE);
if msg == MSG_NOMESSAGE { if msg == MSG_NOMESSAGE {
break; break;
} else { } else {
@@ -310,63 +358,38 @@ impl ComCtlProgressWindow {
} }
} }
progress progress
}
} }
fn show_com_ctl_progress_dialog(rx: Receiver<i16>, window_title: &str, content: &str) { unsafe extern "system" fn task_dialog_callback(
let mut window_title = WString::from_str(window_title); hwnd: HWND,
let mut content = WString::from_str(content); msg: TASKDIALOG_NOTIFICATIONS,
_wparam: WPARAM,
_lparam: LPARAM,
lp_ref_data: isize,
) -> HRESULT {
let raw = lp_ref_data as *mut ComCtlProgressWindow;
let mut ok_text_buf = WString::from_str("Hide"); if let Some(me) = raw.as_mut() {
let mut td_btn = w::TASKDIALOG_BUTTON::default(); if msg == TDN_TIMER {
td_btn.set_nButtonID(co::DLGID::OK.into()); let next_message = drain_and_get_next_message(&me.rx);
td_btn.set_pszButtonText(Some(&mut ok_text_buf));
let mut custom_btns = Vec::with_capacity(1);
custom_btns.push(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));
// 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;
config.pfCallback = Some(task_dialog_callback);
let _ = w::TaskDialogIndirect(&config, None);
}
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 };
if msg == co::TDN::TIMER {
let next_message = me.get_next_message();
if next_message == MSG_CLOSE { if next_message == MSG_CLOSE {
let _ = hwnd.EndDialog(0); let _ = EndDialog(hwnd, 0);
return co::HRESULT::S_OK;
} else if next_message == MSG_INDEFINITE { } else if next_message == MSG_INDEFINITE {
hwnd.SendMessage(MsgSetProgressMarqueeOnOff { is_marquee_on: true }); SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_MARQUEE.0 as u32, Some(WPARAM(1)), Some(LPARAM(0)));
hwnd.SendMessage(MsgSetProgressMarqueeMode { is_marquee_on: true }); SendMessageW(hwnd, TDM_SET_MARQUEE_PROGRESS_BAR.0 as u32, Some(WPARAM(1)), Some(LPARAM(0)));
me.set_progress(MSG_INDEFINITE); me.last_progress = MSG_INDEFINITE;
} else if next_message >= 0 { } else if next_message >= 0 {
if me.get_progress() < 0 { if me.last_progress < 0 {
hwnd.SendMessage(MsgSetProgressMarqueeOnOff { is_marquee_on: false }); SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_MARQUEE.0 as u32, Some(WPARAM(0)), Some(LPARAM(0)));
hwnd.SendMessage(MsgSetProgressMarqueeMode { is_marquee_on: false }); 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] #[test]
@@ -374,8 +397,16 @@ extern "system" fn task_dialog_callback(hwnd: w::HWND, msg: co::TDN, _: usize, _
fn show_test_gif() { fn show_test_gif() {
let rd = std::fs::read(r"C:\Source\Clowd\artwork\splash.gif").unwrap(); let rd = std::fs::read(r"C:\Source\Clowd\artwork\splash.gif").unwrap();
let tx = show_splash_dialog("osu!".to_string(), Some(rd)); let tx = show_splash_dialog("osu!".to_string(), Some(rd));
tx.send(80).unwrap(); let _ = tx.send(25);
std::thread::sleep(std::time::Duration::from_secs(6)); 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] #[test]

View File

@@ -1,34 +1,88 @@
use anyhow::Result; use anyhow::Result;
use windows::core::{PCWSTR, PWSTR}; 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> { pub fn string_to_u16<P: AsRef<str>>(input: P) -> Vec<u16> {
let input = input.as_ref(); let input = input.as_ref();
input.encode_utf16().chain(Some(0)).collect::<Vec<u16>>() 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]; fn to_wide_slice(&self) -> &[u16];
} }
impl WideString for PWSTR { impl WideStringRef for PWSTR {
fn to_wide_slice(&self) -> &[u16] { fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() } unsafe { self.as_wide() }
} }
} }
impl WideString for PCWSTR { impl WideStringRef for PCWSTR {
fn to_wide_slice(&self) -> &[u16] { fn to_wide_slice(&self) -> &[u16] {
unsafe { self.as_wide() } unsafe { self.as_wide() }
} }
} }
impl WideString for Vec<u16> { impl WideStringRef for Vec<u16> {
fn to_wide_slice(&self) -> &[u16] { fn to_wide_slice(&self) -> &[u16] {
self.as_ref() self.as_ref()
} }
} }
impl WideString for &Vec<u16> { impl WideStringRef for &Vec<u16> {
fn to_wide_slice(&self) -> &[u16] { fn to_wide_slice(&self) -> &[u16] {
self.as_ref() 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] { fn to_wide_slice(&self) -> &[u16] {
self.as_ref() 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 slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos]; let trimmed_slice = &slice[..null_pos];
String::from_utf16_lossy(trimmed_slice) 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 slice = input.to_wide_slice();
let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len()); let null_pos = slice.iter().position(|&x| x == 0).unwrap_or(slice.len());
let trimmed_slice = &slice[..null_pos]; let trimmed_slice = &slice[..null_pos];
Ok(String::from_utf16(trimmed_slice)?) Ok(String::from_utf16(trimmed_slice)?)
} }
// pub fn pwstr_to_string(input: PWSTR) -> Result<String> { // pub fn pwstr_to_string(input: PWSTR) -> Result<String> {
// unsafe { // unsafe {
// let hstring = input.to_hstring(); // let hstring = input.to_hstring();

View File

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