From 821435c8a33cbd28ba68d0c838819499afb69964 Mon Sep 17 00:00:00 2001 From: darktohka Date: Tue, 17 Mar 2026 00:35:05 +0200 Subject: [PATCH] Implement HiDPI scaling --- rust/Cargo.toml | 3 + .../clean_flash_common/src/file_util.rs | 150 ++++++++++++++++- .../crates/clean_flash_common/src/registry.rs | 4 +- .../clean_flash_common/src/uninstaller.rs | 45 +++++- .../clean_flash_common/src/update_checker.rs | 141 +++++++++++++++- rust/crates/clean_flash_installer/build.rs | 6 + .../clean_flash_installer/src/install_form.rs | 153 ++++++++---------- rust/crates/clean_flash_installer/src/main.rs | 19 ++- rust/crates/clean_flash_ui/src/lib.rs | 14 ++ rust/crates/clean_flash_ui/src/renderer.rs | 23 +++ .../clean_flash_ui/src/widgets/button.rs | 4 +- .../clean_flash_ui/src/widgets/checkbox.rs | 8 +- .../clean_flash_ui/src/widgets/label.rs | 4 +- rust/crates/clean_flash_uninstaller/build.rs | 6 + .../clean_flash_uninstaller/src/main.rs | 14 +- .../src/uninstall_form.rs | 62 ++++--- 16 files changed, 522 insertions(+), 134 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 70147e7..1f65018 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -14,13 +14,16 @@ image = { version = "0.25", default-features = false, features = ["png"] } minifb = "0.28" windows-sys = { version = "0.61", features = [ "Win32_Foundation", + "Win32_Networking_WinHttp", "Win32_Security", "Win32_Security_Authorization", "Win32_System_LibraryLoader", + "Win32_System_RestartManager", "Win32_System_Threading", "Win32_System_ProcessStatus", "Win32_Storage_FileSystem", "Win32_System_SystemInformation", + "Win32_UI_HiDpi", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", ] } diff --git a/rust/crates/clean_flash_common/src/file_util.rs b/rust/crates/clean_flash_common/src/file_util.rs index 58fa656..613fa11 100644 --- a/rust/crates/clean_flash_common/src/file_util.rs +++ b/rust/crates/clean_flash_common/src/file_util.rs @@ -102,16 +102,154 @@ fn try_clear_readonly_and_delete(path: &Path) -> bool { } fn try_take_ownership_and_delete(path: &Path) -> bool { - // On Windows we could use SetNamedSecurityInfo to take ownership. - // For simplicity the Rust port clears read-only and retries. + use windows_sys::Win32::Foundation::{CloseHandle, LocalFree}; + use windows_sys::Win32::Security::{ + GetTokenInformation, TokenUser, DACL_SECURITY_INFORMATION, + OWNER_SECURITY_INFORMATION, TOKEN_QUERY, TOKEN_USER, + }; + use windows_sys::Win32::Security::Authorization::{ + SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, + SE_FILE_OBJECT, SET_ACCESS, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W, + }; + use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; + + // Ensure SeTakeOwnershipPrivilege is enabled (idempotent). + crate::winapi_helpers::allow_modifications(); + + let path_wide: Vec = path + .to_string_lossy() + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + unsafe { + let mut token = std::ptr::null_mut(); + if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 { + return try_clear_readonly_and_delete(path); + } + + // Retrieve the current user's SID from the process token. + let mut buf = vec![0u8; 512]; + let mut returned = 0u32; + let ok = GetTokenInformation( + token, + TokenUser, + buf.as_mut_ptr() as *mut _, + buf.len() as u32, + &mut returned, + ); + CloseHandle(token); + + if ok == 0 { + return try_clear_readonly_and_delete(path); + } + + let token_user = &*(buf.as_ptr() as *const TOKEN_USER); + let sid = token_user.User.Sid; + + // Transfer ownership of the file to the current user. + SetNamedSecurityInfoW( + path_wide.as_ptr() as *mut _, + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION, + sid, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + + // Build a new DACL that grants FullControl to the current user. + let mut ea = EXPLICIT_ACCESS_W { + grfAccessPermissions: 0x001F_01FF, // FILE_ALL_ACCESS + grfAccessMode: SET_ACCESS, + grfInheritance: 0, // NO_INHERITANCE + Trustee: TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: 0, // NO_MULTIPLE_TRUSTEE + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: sid as *mut u16, + }, + }; + let mut new_dacl: *mut windows_sys::Win32::Security::ACL = std::ptr::null_mut(); + SetEntriesInAclW(1, &mut ea, std::ptr::null_mut(), &mut new_dacl); + + if !new_dacl.is_null() { + SetNamedSecurityInfoW( + path_wide.as_ptr() as *mut _, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + new_dacl, + std::ptr::null_mut(), + ); + LocalFree(new_dacl as *mut _); + } + } + try_clear_readonly_and_delete(path) } fn kill_locking_processes(path: &Path) { - // Use taskkill as a best-effort approach. - // The C# original enumerates all open handles, which requires complex - // NT API calls. A simplified approach is acceptable for the port. - let _ = path; // Locking-process detection is a best-effort no-op here. + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::RestartManager::{ + RmEndSession, RmGetList, RmRegisterResources, RmStartSession, RM_PROCESS_INFO, + }; + use windows_sys::Win32::System::Threading::{ + OpenProcess, TerminateProcess, WaitForSingleObject, PROCESS_TERMINATE, + }; + + let path_wide: Vec = path + .to_string_lossy() + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + unsafe { + let mut session: u32 = 0; + // CCH_RM_SESSION_KEY = 32 chars; +1 for null terminator. + let mut session_key = [0u16; 33]; + if RmStartSession(&mut session, 0, session_key.as_mut_ptr()) != 0 { + return; + } + + let file_ptr = path_wide.as_ptr(); + let files = [file_ptr]; + RmRegisterResources( + session, + 1, + files.as_ptr(), + 0, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + ); + + let mut n_needed: u32 = 0; + let mut n_info: u32 = 10; + let mut procs: [RM_PROCESS_INFO; 10] = std::mem::zeroed(); + let mut reboot_reasons: u32 = 0; + RmGetList( + session, + &mut n_needed, + &mut n_info, + procs.as_mut_ptr(), + &mut reboot_reasons, + ); + + for proc_info in procs.iter().take(n_info as usize) { + let pid = proc_info.Process.dwProcessId; + let handle = OpenProcess(PROCESS_TERMINATE, 0, pid); + if !handle.is_null() { + TerminateProcess(handle, 1); + WaitForSingleObject(handle, 5000); + CloseHandle(handle); + } + } + + RmEndSession(session); + } } fn is_dir_empty(path: &Path) -> bool { diff --git a/rust/crates/clean_flash_common/src/registry.rs b/rust/crates/clean_flash_common/src/registry.rs index c740214..72413d4 100644 --- a/rust/crates/clean_flash_common/src/registry.rs +++ b/rust/crates/clean_flash_common/src/registry.rs @@ -10,7 +10,9 @@ pub fn apply_registry(entries: &[&str]) -> Result<(), InstallError> { let content = format!("Windows Registry Editor Version 5.00\n\n{}", filled); let temp_dir = std::env::temp_dir(); - let reg_file = temp_dir.join("cleanflash_reg.tmp"); + // Include the process ID to avoid collisions when two instances run concurrently, + // matching the unique-file guarantee of C#'s Path.GetTempFileName(). + let reg_file = temp_dir.join(format!("cleanflash_reg_{}.tmp", std::process::id())); // Write as UTF-16LE with BOM (Windows .reg format). { diff --git a/rust/crates/clean_flash_common/src/uninstaller.rs b/rust/crates/clean_flash_common/src/uninstaller.rs index d42e68f..f16d6f0 100644 --- a/rust/crates/clean_flash_common/src/uninstaller.rs +++ b/rust/crates/clean_flash_common/src/uninstaller.rs @@ -146,6 +146,28 @@ fn delete_flash_center() { } } } + + // Remove Quick Launch shortcuts from Internet Explorer. + if let Ok(appdata) = env::var("APPDATA") { + file_util::recursive_delete( + &PathBuf::from(&appdata) + .join("Microsoft") + .join("Internet Explorer") + .join("Quick Launch"), + Some("Flash Center.lnk"), + ); + } + + // Remove Flash Player shortcut from the user's Start Menu root. + if let Ok(appdata) = env::var("APPDATA") { + file_util::delete_file( + &PathBuf::from(appdata) + .join("Microsoft") + .join("Windows") + .join("Start Menu") + .join("Flash Player.lnk"), + ); + } } fn delete_flash_player() { @@ -189,7 +211,10 @@ fn stop_processes() { }; // Enumerate all processes via the snapshot API. - let pids = enumerate_processes(); + let mut pids = enumerate_processes(); + + // Sort by creation time (oldest first), matching C#'s .OrderBy(o => o.StartTime). + pids.sort_by_key(|(pid, _)| get_process_creation_time(*pid)); for (pid, name) in &pids { let lower = name.to_lowercase(); @@ -211,6 +236,24 @@ fn stop_processes() { } } +fn get_process_creation_time(pid: u32) -> u64 { + use windows_sys::Win32::Foundation::{CloseHandle, FILETIME}; + use windows_sys::Win32::System::Threading::{GetProcessTimes, OpenProcess, PROCESS_QUERY_INFORMATION}; + unsafe { + let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); + if handle.is_null() { + return 0; + } + let mut create = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 }; + let mut exit_time = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 }; + let mut kernel_time = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 }; + let mut user_time = FILETIME { dwLowDateTime: 0, dwHighDateTime: 0 }; + GetProcessTimes(handle, &mut create, &mut exit_time, &mut kernel_time, &mut user_time); + CloseHandle(handle); + ((create.dwHighDateTime as u64) << 32) | create.dwLowDateTime as u64 + } +} + fn enumerate_processes() -> Vec<(u32, String)> { use windows_sys::Win32::System::ProcessStatus::{EnumProcesses, GetModuleBaseNameW}; use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; diff --git a/rust/crates/clean_flash_common/src/update_checker.rs b/rust/crates/clean_flash_common/src/update_checker.rs index f7581cd..248d3b2 100644 --- a/rust/crates/clean_flash_common/src/update_checker.rs +++ b/rust/crates/clean_flash_common/src/update_checker.rs @@ -1,6 +1,10 @@ pub const FLASH_VERSION: &str = "34.0.0.330"; pub const VERSION: &str = "34.0.0.330"; +const API_HOST: &str = "api.github.com"; +const API_PATH: &str = "/repos/cleanflash/installer/releases/latest"; +const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"; + pub struct VersionInfo { pub name: String, pub version: String, @@ -8,7 +12,138 @@ pub struct VersionInfo { } pub fn get_latest_version() -> Option { - // The original fetches from the GitHub API. - // Stubbed for the port; real implementation would use ureq or reqwest. - None + let json = fetch_https(API_HOST, API_PATH, USER_AGENT)?; + let name = extract_json_string(&json, "name")?; + let tag = extract_json_string(&json, "tag_name")?; + let url = extract_json_string(&json, "html_url")?; + + // Validate the URL to guard against a malicious/unexpected response. + if !url.starts_with("https://") { + return None; + } + + println!("Latest release: {} ({}) {}", name, tag, url); + Some(VersionInfo { name, version: tag, url }) +} + +/// Perform an HTTPS GET request using WinHTTP and return the response body as UTF-8. +fn fetch_https(host: &str, path: &str, user_agent: &str) -> Option { + use windows_sys::Win32::Networking::WinHttp::{ + WinHttpCloseHandle, WinHttpConnect, WinHttpOpen, WinHttpOpenRequest, + WinHttpQueryDataAvailable, WinHttpReadData, WinHttpReceiveResponse, + WinHttpSendRequest, WINHTTP_FLAG_SECURE, + }; + + let wide = |s: &str| -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() }; + + let agent_w = wide(user_agent); + let host_w = wide(host); + let path_w = wide(path); + + unsafe { + // Open session with system default proxy settings (WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0). + let session = WinHttpOpen(agent_w.as_ptr(), 0, std::ptr::null(), std::ptr::null(), 0); + if session.is_null() { + return None; + } + + let connection = WinHttpConnect(session, host_w.as_ptr(), 443, 0); + if connection.is_null() { + WinHttpCloseHandle(session); + return None; + } + + // Open a GET request over HTTPS (null verb = GET, null version = HTTP/1.1). + let request = WinHttpOpenRequest( + connection, + std::ptr::null(), + path_w.as_ptr(), + std::ptr::null(), + std::ptr::null(), + std::ptr::null(), + WINHTTP_FLAG_SECURE, + ); + if request.is_null() { + WinHttpCloseHandle(connection); + WinHttpCloseHandle(session); + return None; + } + + if WinHttpSendRequest(request, std::ptr::null(), 0, std::ptr::null(), 0, 0, 0) == 0 { + WinHttpCloseHandle(request); + WinHttpCloseHandle(connection); + WinHttpCloseHandle(session); + return None; + } + + if WinHttpReceiveResponse(request, std::ptr::null_mut()) == 0 { + WinHttpCloseHandle(request); + WinHttpCloseHandle(connection); + WinHttpCloseHandle(session); + return None; + } + + let mut response: Vec = Vec::new(); + loop { + let mut available: u32 = 0; + if WinHttpQueryDataAvailable(request, &mut available) == 0 || available == 0 { + break; + } + let offset = response.len(); + response.resize(offset + available as usize, 0); + let mut read: u32 = 0; + if WinHttpReadData( + request, + response[offset..].as_mut_ptr() as *mut _, + available, + &mut read, + ) == 0 + { + break; + } + response.truncate(offset + read as usize); + } + + WinHttpCloseHandle(request); + WinHttpCloseHandle(connection); + WinHttpCloseHandle(session); + + String::from_utf8(response).ok() + } +} + +/// Extract a JSON string value for the given key from a JSON object. +/// Handles the escape sequences that appear in GitHub API responses. +fn extract_json_string(json: &str, key: &str) -> Option { + let search = format!("\"{}\"", key); + let key_pos = json.find(&search)?; + let rest = &json[key_pos + search.len()..]; + let colon = rest.find(':')?; + let rest = rest[colon + 1..].trim_start(); + if !rest.starts_with('"') { + return None; + } + let rest = &rest[1..]; + let mut result = String::new(); + let mut chars = rest.chars(); + loop { + match chars.next()? { + '"' => break, + '\\' => match chars.next()? { + '"' => result.push('"'), + '\\' => result.push('\\'), + '/' => result.push('/'), + 'n' => result.push('\n'), + 'r' => result.push('\r'), + 't' => result.push('\t'), + c => { + result.push('\\'); + result.push(c); + } + }, + c => result.push(c), + } + } + Some(result) } + diff --git a/rust/crates/clean_flash_installer/build.rs b/rust/crates/clean_flash_installer/build.rs index bce6ed0..274c4e0 100644 --- a/rust/crates/clean_flash_installer/build.rs +++ b/rust/crates/clean_flash_installer/build.rs @@ -30,6 +30,12 @@ fn main() { + + + True/PM + PerMonitorV2 + + "#); let _ = res.compile(); diff --git a/rust/crates/clean_flash_installer/src/install_form.rs b/rust/crates/clean_flash_installer/src/install_form.rs index a7a464c..c2ba7d7 100644 --- a/rust/crates/clean_flash_installer/src/install_form.rs +++ b/rust/crates/clean_flash_installer/src/install_form.rs @@ -54,6 +54,7 @@ pub struct ProgressState { /// Full application state for the installer form. pub struct InstallForm { + scale: f32, pub panel: Panel, // Header pub title_text: String, @@ -109,7 +110,24 @@ pub struct InstallForm { } impl InstallForm { - pub fn new() -> Self { + pub fn new(scale: f32) -> Self { + // Integer coordinate scaler. + let s = |v: i32| (v as f32 * scale).round() as i32; + // Font size / spacing scaler. + let sf = |v: f32| v * scale; + // Scaled label helper: font_size is the logical total (base+offset). + let lbl = |x: i32, y: i32, text: &str, size: f32| { + let mut l = Label::new(s(x), s(y), text, sf(size)); + l.line_spacing = sf(2.0); + l + }; + // Scaled button helper. + let btn = |x: i32, y: i32, w: i32, h: i32, text: &str| GradientButton { + font_size: sf(13.0), + ..GradientButton::new(s(x), s(y), s(w), s(h), text) + }; + // Scaled checkbox helper. + let chk = |x: i32, y: i32| ImageCheckBox::with_size(s(x), s(y), s(21)); let version = update_checker::FLASH_VERSION; let title_text = "Clean Flash Player".to_string(); let subtitle_text = format!("built from version {} (China)", version); @@ -123,102 +141,63 @@ impl InstallForm { let fonts = FontManager::new(); Self { + scale, panel: Panel::Disclaimer, title_text, subtitle_text, flash_logo, checkbox_on, checkbox_off, - prev_button: GradientButton::new(90, 286, 138, 31, "QUIT"), - next_button: GradientButton::new(497, 286, 138, 31, "AGREE"), + prev_button: btn(90, 286, 138, 31, "QUIT"), + next_button: btn(497, 286, 138, 31, "AGREE"), // Disclaimer panel - disclaimer_label: Label::new(PANEL_X + 25, PANEL_Y, DISCLAIMER_TEXT, 13.0), - disclaimer_box: ImageCheckBox::new(PANEL_X, PANEL_Y), + disclaimer_label: lbl(PANEL_X + 25, PANEL_Y, DISCLAIMER_TEXT, 15.0), + disclaimer_box: chk(PANEL_X, PANEL_Y), // Choice panel - browser_ask_label: Label::new( - PANEL_X - 2, - PANEL_Y + 2, - "Which browser plugins would you like to install?", - 13.0, - ), - pepper_box: ImageCheckBox::new(PANEL_X, PANEL_Y + 47), - pepper_label: Label::new( - PANEL_X + 24, - PANEL_Y + 47, - "Pepper API (PPAPI)\n(Chrome/Opera/Brave)", - 13.0, - ), - netscape_box: ImageCheckBox::new(PANEL_X + 186, PANEL_Y + 47), - netscape_label: Label::new( - PANEL_X + 210, - PANEL_Y + 47, - "Netscape API (NPAPI)\n(Firefox/ESR/Waterfox)", - 13.0, - ), - activex_box: ImageCheckBox::new(PANEL_X + 365, PANEL_Y + 47), - activex_label: Label::new( - PANEL_X + 389, - PANEL_Y + 47, - "ActiveX (OCX)\n(IE/Embedded/Desktop)", - 13.0, - ), + browser_ask_label: lbl(PANEL_X - 2, PANEL_Y + 2, "Which browser plugins would you like to install?", 15.0), + pepper_box: chk(PANEL_X, PANEL_Y + 47), + pepper_label: lbl(PANEL_X + 24, PANEL_Y + 47, "Pepper API (PPAPI)\n(Chrome/Opera/Brave)", 15.0), + netscape_box: chk(PANEL_X + 186, PANEL_Y + 47), + netscape_label: lbl(PANEL_X + 210, PANEL_Y + 47, "Netscape API (NPAPI)\n(Firefox/ESR/Waterfox)", 15.0), + activex_box: chk(PANEL_X + 365, PANEL_Y + 47), + activex_label: lbl(PANEL_X + 389, PANEL_Y + 47, "ActiveX (OCX)\n(IE/Embedded/Desktop)", 15.0), // Player choice panel - player_ask_label: Label::new( - PANEL_X - 2, - PANEL_Y + 2, - "Would you like to install the standalone Flash Player?", - 13.0, - ), - player_box: ImageCheckBox::new(PANEL_X, PANEL_Y + 47), - player_label: Label::new( - PANEL_X + 24, - PANEL_Y + 47, - "Install Standalone\nFlash Player", - 13.0, - ), - player_desktop_box: ImageCheckBox::new(PANEL_X + 186, PANEL_Y + 47), - player_desktop_label: Label::new( - PANEL_X + 210, - PANEL_Y + 47, - "Create Shortcuts\non Desktop", - 13.0, - ), - player_start_menu_box: ImageCheckBox::new(PANEL_X + 365, PANEL_Y + 47), - player_start_menu_label: Label::new( - PANEL_X + 389, - PANEL_Y + 47, - "Create Shortcuts\nin Start Menu", - 13.0, - ), + player_ask_label: lbl(PANEL_X - 2, PANEL_Y + 2, "Would you like to install the standalone Flash Player?", 15.0), + player_box: chk(PANEL_X, PANEL_Y + 47), + player_label: lbl(PANEL_X + 24, PANEL_Y + 47, "Install Standalone\nFlash Player", 15.0), + player_desktop_box: chk(PANEL_X + 186, PANEL_Y + 47), + player_desktop_label: lbl(PANEL_X + 210, PANEL_Y + 47, "Create Shortcuts\non Desktop", 15.0), + player_start_menu_box: chk(PANEL_X + 365, PANEL_Y + 47), + player_start_menu_label: lbl(PANEL_X + 389, PANEL_Y + 47, "Create Shortcuts\nin Start Menu", 15.0), // Debug choice panel - debug_ask_label: Label::new( + debug_ask_label: lbl( PANEL_X - 2, PANEL_Y + 2, "Would you like to install the debug version of Clean Flash Player?\n\ You should only choose the debug version if you are planning to create Flash applications.\n\ If you are not sure, simply press NEXT.", - 13.0, + 15.0, ), - debug_button: GradientButton::new(PANEL_X + 186, PANEL_Y + 65, 176, 31, "INSTALL DEBUG VERSION"), + debug_button: btn(PANEL_X + 186, PANEL_Y + 65, 176, 31, "INSTALL DEBUG VERSION"), debug_chosen: false, // Before install panel - before_install_label: Label::new(PANEL_X + 3, PANEL_Y + 2, "", 13.0), + before_install_label: lbl(PANEL_X + 3, PANEL_Y + 2, "", 15.0), // Install panel - install_header_label: Label::new(PANEL_X + 3, PANEL_Y, "Installation in progress...", 13.0), - progress_label: Label::new(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 13.0), - progress_bar: ProgressBar::new(PANEL_X + 49, PANEL_Y + 58, 451, 23), + install_header_label: lbl(PANEL_X + 3, PANEL_Y, "Installation in progress...", 15.0), + progress_label: lbl(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 15.0), + progress_bar: ProgressBar::new(s(PANEL_X + 49), s(PANEL_Y + 58), s(451), s(23)), // Complete panel - complete_label: Label::new(PANEL_X, PANEL_Y, "", 13.0), + complete_label: lbl(PANEL_X, PANEL_Y, "", 15.0), // Failure panel - failure_text_label: Label::new( + failure_text_label: lbl( PANEL_X + 3, PANEL_Y + 2, "Oops! The installation process has encountered an unexpected problem.\n\ The following details could be useful. Press the Retry button to try again.", - 13.0, + 15.0, ), failure_detail: String::new(), - copy_error_button: GradientButton::new(PANEL_X + 441, PANEL_Y + 58, 104, 31, "COPY"), + copy_error_button: btn(PANEL_X + 441, PANEL_Y + 58, 104, 31, "COPY"), progress_state: Arc::new(Mutex::new(ProgressState { label: "Preparing...".into(), value: 0, @@ -230,6 +209,11 @@ The following details could be useful. Press the Retry button to try again.", } } + /// Scale a logical integer coordinate to physical pixels. + fn s(&self, v: i32) -> i32 { (v as f32 * self.scale).round() as i32 } + /// Scale a logical float value to physical pixels. + fn sf(&self, v: f32) -> f32 { v * self.scale } + /// Called each frame: handle input, update state, draw. pub fn update_and_draw( &mut self, @@ -257,30 +241,32 @@ The following details could be useful. Press the Retry button to try again.", renderer.clear(BG_COLOR); // Header: flash logo. - renderer.draw_image(90, 36, &self.flash_logo); + let lw = (self.flash_logo.width as f32 * self.scale) as i32; + let lh = (self.flash_logo.height as f32 * self.scale) as i32; + renderer.draw_image_scaled(self.s(90), self.s(36), lw, lh, &self.flash_logo); // Title. self.fonts.draw_text( renderer, - 233, - 54, + self.s(233), + self.s(54), &self.title_text, - 32.0, // ~24pt Segoe UI + self.sf(32.0), FG_COLOR, ); // Subtitle. self.fonts.draw_text( renderer, - 280, - 99, + self.s(280), + self.s(99), &self.subtitle_text, - 17.0, // ~13pt Segoe UI + self.sf(17.0), FG_COLOR, ); - // Separator line at y=270. - renderer.fill_rect(0, 270, WIDTH as i32, 1, Renderer::rgb(105, 105, 105)); + // Separator line. + renderer.fill_rect(0, self.s(270), renderer.width as i32, self.s(1).max(1), Renderer::rgb(105, 105, 105)); // Draw current panel. match self.panel { @@ -628,7 +614,6 @@ including Clean Flash Player and older versions of Adobe Flash Player." fn draw_failure(&self, r: &mut Renderer) { self.failure_text_label.draw(r, &self.fonts); // Draw error detail as clipped text. - let detail_y = PANEL_Y + 44; let detail_text = if self.failure_detail.len() > 300 { &self.failure_detail[..300] } else { @@ -636,12 +621,12 @@ including Clean Flash Player and older versions of Adobe Flash Player." }; self.fonts.draw_text_multiline( r, - PANEL_X + 4, - detail_y, + self.s(PANEL_X + 4), + self.s(PANEL_Y + 44), detail_text, - 11.0, + self.sf(11.0), FG_COLOR, - 1.0, + self.sf(1.0), ); self.copy_error_button.draw(r, &self.fonts); } diff --git a/rust/crates/clean_flash_installer/src/main.rs b/rust/crates/clean_flash_installer/src/main.rs index e317579..6a4d28d 100644 --- a/rust/crates/clean_flash_installer/src/main.rs +++ b/rust/crates/clean_flash_installer/src/main.rs @@ -1,4 +1,4 @@ -#![windows_subsystem = "windows"] +//#![windows_subsystem = "windows"] mod install_flags; mod install_form; @@ -14,10 +14,16 @@ fn main() { clean_flash_common::update_checker::FLASH_VERSION ); + // Query DPI before creating any window. The manifest already marks the process + // as per-monitor v2 DPI aware, so GetDpiForSystem returns the real hardware DPI. + let scale = clean_flash_ui::get_dpi_scale(); + let phys_w = (WIDTH as f32 * scale).round() as usize; + let phys_h = (HEIGHT as f32 * scale).round() as usize; + let mut window = Window::new( &title, - WIDTH, - HEIGHT, + phys_w, + phys_h, WindowOptions { resize: false, ..WindowOptions::default() @@ -31,8 +37,9 @@ fn main() { // Cap at ~60 fps. window.set_target_fps(60); - let mut renderer = Renderer::new(WIDTH, HEIGHT); - let mut form = InstallForm::new(); + // Renderer operates at physical resolution; the form layout is scaled accordingly. + let mut renderer = Renderer::new(phys_w, phys_h); + let mut form = InstallForm::new(scale); while window.is_open() && !window.is_key_down(Key::Escape) { let (mx, my) = window @@ -43,7 +50,7 @@ fn main() { form.update_and_draw(&mut renderer, mx as i32, my as i32, mouse_down); window - .update_with_buffer(&renderer.buffer, WIDTH, HEIGHT) + .update_with_buffer(&renderer.buffer, phys_w, phys_h) .expect("Failed to update window buffer"); } } diff --git a/rust/crates/clean_flash_ui/src/lib.rs b/rust/crates/clean_flash_ui/src/lib.rs index f92686e..9794928 100644 --- a/rust/crates/clean_flash_ui/src/lib.rs +++ b/rust/crates/clean_flash_ui/src/lib.rs @@ -5,6 +5,20 @@ pub mod widgets; pub use font::FontManager; pub use renderer::Renderer; +/// Query the system DPI scale factor (1.0 = 100%, 1.5 = 150%, 2.0 = 200%). +/// The process must already be DPI-aware (declared in the manifest) for this +/// to return the true hardware DPI rather than the virtualised 96. +pub fn get_dpi_scale() -> f32 { + #[cfg(target_os = "windows")] + unsafe { + use windows_sys::Win32::UI::HiDpi::GetDpiForSystem; + let dpi = GetDpiForSystem(); + if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 } + } + #[cfg(not(target_os = "windows"))] + { 1.0 } +} + /// Set the window icon from the icon resource already embedded in the binary /// by `winresource` (resource ID 1). No-op on non-Windows platforms. pub fn set_window_icon(window: &minifb::Window) { diff --git a/rust/crates/clean_flash_ui/src/renderer.rs b/rust/crates/clean_flash_ui/src/renderer.rs index 184c11c..a16e00d 100644 --- a/rust/crates/clean_flash_ui/src/renderer.rs +++ b/rust/crates/clean_flash_ui/src/renderer.rs @@ -109,6 +109,29 @@ impl Renderer { } } + /// Draw an RGBA image scaled to (w, h) at (x, y) using nearest-neighbor interpolation. + pub fn draw_image_scaled(&mut self, x: i32, y: i32, w: i32, h: i32, img: &RgbaImage) { + if w <= 0 || h <= 0 || img.width == 0 || img.height == 0 { + return; + } + for dy in 0..h { + for dx in 0..w { + let src_x = (dx as f32 * img.width as f32 / w as f32) as usize; + let src_y = (dy as f32 * img.height as f32 / h as f32) as usize; + let idx = (src_y * img.width + src_x) * 4; + let r = img.data[idx]; + let g = img.data[idx + 1]; + let b = img.data[idx + 2]; + let a = img.data[idx + 3]; + if a == 255 { + self.set_pixel(x + dx, y + dy, Self::rgb(r, g, b)); + } else if a > 0 { + self.blend_pixel(x + dx, y + dy, Self::rgb(r, g, b), a); + } + } + } + } + /// Draw an RGBA image onto the framebuffer at (x, y). pub fn draw_image(&mut self, x: i32, y: i32, img: &RgbaImage) { for iy in 0..img.height as i32 { diff --git a/rust/crates/clean_flash_ui/src/widgets/button.rs b/rust/crates/clean_flash_ui/src/widgets/button.rs index 9698061..c2e3d1c 100644 --- a/rust/crates/clean_flash_ui/src/widgets/button.rs +++ b/rust/crates/clean_flash_ui/src/widgets/button.rs @@ -6,6 +6,7 @@ use crate::renderer::Renderer; pub struct GradientButton { pub rect: Rect, pub text: String, + pub font_size: f32, pub color1: u32, pub color2: u32, pub back_color: u32, @@ -23,6 +24,7 @@ impl GradientButton { Self { rect: Rect::new(x, y, w, h), text: text.to_string(), + font_size: 13.0, color1: Renderer::rgb(118, 118, 118), color2: Renderer::rgb(81, 81, 81), back_color: Renderer::rgb(0, 0, 0), @@ -77,7 +79,7 @@ impl GradientButton { renderer.draw_rect(r.x, r.y, r.w, r.h, bg); // Measure text to centre it. - let font_size = 13.0; + let font_size = self.font_size; let (tw, th) = fonts.measure_text(&self.text, font_size); let tx = r.x + ((r.w as f32 - tw) / 2.0) as i32; let ty = r.y + ((r.h as f32 - th) / 2.0) as i32; diff --git a/rust/crates/clean_flash_ui/src/widgets/checkbox.rs b/rust/crates/clean_flash_ui/src/widgets/checkbox.rs index 5cab729..2409f96 100644 --- a/rust/crates/clean_flash_ui/src/widgets/checkbox.rs +++ b/rust/crates/clean_flash_ui/src/widgets/checkbox.rs @@ -11,8 +11,12 @@ pub struct ImageCheckBox { impl ImageCheckBox { pub fn new(x: i32, y: i32) -> Self { + Self::with_size(x, y, 21) + } + + pub fn with_size(x: i32, y: i32, size: i32) -> Self { Self { - rect: Rect::new(x, y, 21, 21), + rect: Rect::new(x, y, size, size), checked: true, enabled: true, visible: true, @@ -43,7 +47,7 @@ impl ImageCheckBox { unchecked_img }; if img.width > 0 && img.height > 0 { - renderer.draw_image(self.rect.x, self.rect.y, img); + renderer.draw_image_scaled(self.rect.x, self.rect.y, self.rect.w, self.rect.h, img); } else { // Fallback: draw a simple square. let bg = if self.checked { diff --git a/rust/crates/clean_flash_ui/src/widgets/label.rs b/rust/crates/clean_flash_ui/src/widgets/label.rs index 4482db7..7ff644c 100644 --- a/rust/crates/clean_flash_ui/src/widgets/label.rs +++ b/rust/crates/clean_flash_ui/src/widgets/label.rs @@ -8,6 +8,7 @@ pub struct Label { pub text: String, pub color: u32, pub font_size: f32, + pub line_spacing: f32, pub visible: bool, } @@ -18,6 +19,7 @@ impl Label { text: text.to_string(), color: Renderer::rgb(245, 245, 245), font_size, + line_spacing: 2.0, visible: true, } } @@ -33,7 +35,7 @@ impl Label { &self.text, self.font_size, self.color, - 2.0, + self.line_spacing, ); } diff --git a/rust/crates/clean_flash_uninstaller/build.rs b/rust/crates/clean_flash_uninstaller/build.rs index 61f4632..075d8a4 100644 --- a/rust/crates/clean_flash_uninstaller/build.rs +++ b/rust/crates/clean_flash_uninstaller/build.rs @@ -28,6 +28,12 @@ fn main() { + + + True/PM + PerMonitorV2 + + "#); let _ = res.compile(); diff --git a/rust/crates/clean_flash_uninstaller/src/main.rs b/rust/crates/clean_flash_uninstaller/src/main.rs index bf6bb0d..834f422 100644 --- a/rust/crates/clean_flash_uninstaller/src/main.rs +++ b/rust/crates/clean_flash_uninstaller/src/main.rs @@ -12,10 +12,14 @@ fn main() { clean_flash_common::update_checker::FLASH_VERSION ); + let scale = clean_flash_ui::get_dpi_scale(); + let phys_w = (WIDTH as f32 * scale).round() as usize; + let phys_h = (HEIGHT as f32 * scale).round() as usize; + let mut window = Window::new( &title, - WIDTH, - HEIGHT, + phys_w, + phys_h, WindowOptions { resize: false, ..WindowOptions::default() @@ -28,8 +32,8 @@ fn main() { window.set_target_fps(60); - let mut renderer = Renderer::new(WIDTH, HEIGHT); - let mut form = UninstallForm::new(); + let mut renderer = Renderer::new(phys_w, phys_h); + let mut form = UninstallForm::new(scale); while window.is_open() && !window.is_key_down(Key::Escape) { let (mx, my) = window @@ -40,7 +44,7 @@ fn main() { form.update_and_draw(&mut renderer, mx as i32, my as i32, mouse_down); window - .update_with_buffer(&renderer.buffer, WIDTH, HEIGHT) + .update_with_buffer(&renderer.buffer, phys_w, phys_h) .expect("Failed to update window buffer"); } } diff --git a/rust/crates/clean_flash_uninstaller/src/uninstall_form.rs b/rust/crates/clean_flash_uninstaller/src/uninstall_form.rs index dd3cb99..6da1644 100644 --- a/rust/crates/clean_flash_uninstaller/src/uninstall_form.rs +++ b/rust/crates/clean_flash_uninstaller/src/uninstall_form.rs @@ -39,6 +39,7 @@ struct ProgressState { } pub struct UninstallForm { + scale: f32, panel: Panel, title_text: String, subtitle_text: String, @@ -64,37 +65,45 @@ pub struct UninstallForm { } impl UninstallForm { - pub fn new() -> Self { + pub fn new(scale: f32) -> Self { + let s = |v: i32| (v as f32 * scale).round() as i32; + let sf = |v: f32| v * scale; + let lbl = |x: i32, y: i32, text: &str, size: f32| { + let mut l = Label::new(s(x), s(y), text, sf(size)); + l.line_spacing = sf(2.0); + l + }; + let btn = |x: i32, y: i32, w: i32, h: i32, text: &str| GradientButton { + font_size: sf(13.0), + ..GradientButton::new(s(x), s(y), s(w), s(h), text) + }; + let version = update_checker::FLASH_VERSION; let flash_logo = load_resource_image("flashLogo.png"); let fonts = FontManager::new(); Self { + scale, panel: Panel::BeforeInstall, title_text: "Clean Flash Player".into(), subtitle_text: format!("built from version {} (China)", version), flash_logo, - prev_button: GradientButton::new(90, 286, 138, 31, "QUIT"), - next_button: GradientButton::new(497, 286, 138, 31, "UNINSTALL"), - before_label: Label::new(PANEL_X + 3, PANEL_Y + 2, BEFORE_TEXT, 13.0), - progress_header: Label::new( - PANEL_X + 3, - PANEL_Y, - "Uninstallation in progress...", - 13.0, - ), - progress_label: Label::new(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 13.0), - progress_bar: ProgressBar::new(PANEL_X + 49, PANEL_Y + 58, 451, 23), - complete_label: Label::new(PANEL_X, PANEL_Y, COMPLETE_TEXT, 13.0), - failure_text_label: Label::new( + prev_button: btn(90, 286, 138, 31, "QUIT"), + next_button: btn(497, 286, 138, 31, "UNINSTALL"), + before_label: lbl(PANEL_X + 3, PANEL_Y + 2, BEFORE_TEXT, 15.0), + progress_header: lbl(PANEL_X + 3, PANEL_Y, "Uninstallation in progress...", 15.0), + progress_label: lbl(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 15.0), + progress_bar: ProgressBar::new(s(PANEL_X + 49), s(PANEL_Y + 58), s(451), s(23)), + complete_label: lbl(PANEL_X, PANEL_Y, COMPLETE_TEXT, 15.0), + failure_text_label: lbl( PANEL_X + 3, PANEL_Y + 2, "Oops! The installation process has encountered an unexpected problem.\n\ The following details could be useful. Press the Retry button to try again.", - 13.0, + 15.0, ), failure_detail: String::new(), - copy_error_button: GradientButton::new(PANEL_X + 441, PANEL_Y + 58, 104, 31, "COPY"), + copy_error_button: btn(PANEL_X + 441, PANEL_Y + 58, 104, 31, "COPY"), progress_state: Arc::new(Mutex::new(ProgressState { label: "Preparing...".into(), value: 0, @@ -147,15 +156,17 @@ The following details could be useful. Press the Retry button to try again.", // Draw. renderer.clear(BG_COLOR); - renderer.draw_image(90, 36, &self.flash_logo); + let lw = (self.flash_logo.width as f32 * self.scale) as i32; + let lh = (self.flash_logo.height as f32 * self.scale) as i32; + renderer.draw_image_scaled(self.s(90), self.s(36), lw, lh, &self.flash_logo); self.fonts - .draw_text(renderer, 233, 54, &self.title_text, 32.0, FG_COLOR); + .draw_text(renderer, self.s(233), self.s(54), &self.title_text, self.sf(32.0), FG_COLOR); self.fonts - .draw_text(renderer, 280, 99, &self.subtitle_text, 17.0, FG_COLOR); + .draw_text(renderer, self.s(280), self.s(99), &self.subtitle_text, self.sf(17.0), FG_COLOR); // Separator. - renderer.fill_rect(0, 270, WIDTH as i32, 1, 0x00696969); + renderer.fill_rect(0, self.s(270), renderer.width as i32, self.s(1).max(1), 0x00696969); match self.panel { Panel::BeforeInstall => self.before_label.draw(renderer, &self.fonts), @@ -174,12 +185,12 @@ The following details could be useful. Press the Retry button to try again.", }; self.fonts.draw_text_multiline( renderer, - PANEL_X + 4, - PANEL_Y + 44, + self.s(PANEL_X + 4), + self.s(PANEL_Y + 44), detail, - 11.0, + self.sf(11.0), FG_COLOR, - 1.0, + self.sf(1.0), ); self.copy_error_button.draw(renderer, &self.fonts); } @@ -189,6 +200,9 @@ The following details could be useful. Press the Retry button to try again.", self.next_button.draw(renderer, &self.fonts); } + fn s(&self, v: i32) -> i32 { (v as f32 * self.scale).round() as i32 } + fn sf(&self, v: f32) -> f32 { v * self.scale } + fn start_uninstall(&mut self) { self.panel = Panel::Install; self.prev_button.enabled = false;