Implement HiDPI scaling

master
darktohka 6 days ago
parent f8a1dde721
commit 821435c8a3

@ -14,13 +14,16 @@ image = { version = "0.25", default-features = false, features = ["png"] }
minifb = "0.28" minifb = "0.28"
windows-sys = { version = "0.61", features = [ windows-sys = { version = "0.61", features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_Networking_WinHttp",
"Win32_Security", "Win32_Security",
"Win32_Security_Authorization", "Win32_Security_Authorization",
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_RestartManager",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_System_ProcessStatus", "Win32_System_ProcessStatus",
"Win32_Storage_FileSystem", "Win32_Storage_FileSystem",
"Win32_System_SystemInformation", "Win32_System_SystemInformation",
"Win32_UI_HiDpi",
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
] } ] }

@ -102,16 +102,154 @@ fn try_clear_readonly_and_delete(path: &Path) -> bool {
} }
fn try_take_ownership_and_delete(path: &Path) -> bool { fn try_take_ownership_and_delete(path: &Path) -> bool {
// On Windows we could use SetNamedSecurityInfo to take ownership. use windows_sys::Win32::Foundation::{CloseHandle, LocalFree};
// For simplicity the Rust port clears read-only and retries. 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<u16> = 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) try_clear_readonly_and_delete(path)
} }
fn kill_locking_processes(path: &Path) { fn kill_locking_processes(path: &Path) {
// Use taskkill as a best-effort approach. use windows_sys::Win32::Foundation::CloseHandle;
// The C# original enumerates all open handles, which requires complex use windows_sys::Win32::System::RestartManager::{
// NT API calls. A simplified approach is acceptable for the port. RmEndSession, RmGetList, RmRegisterResources, RmStartSession, RM_PROCESS_INFO,
let _ = path; // Locking-process detection is a best-effort no-op here. };
use windows_sys::Win32::System::Threading::{
OpenProcess, TerminateProcess, WaitForSingleObject, PROCESS_TERMINATE,
};
let path_wide: Vec<u16> = 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 { fn is_dir_empty(path: &Path) -> bool {

@ -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 content = format!("Windows Registry Editor Version 5.00\n\n{}", filled);
let temp_dir = std::env::temp_dir(); 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). // Write as UTF-16LE with BOM (Windows .reg format).
{ {

@ -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() { fn delete_flash_player() {
@ -189,7 +211,10 @@ fn stop_processes() {
}; };
// Enumerate all processes via the snapshot API. // 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 { for (pid, name) in &pids {
let lower = name.to_lowercase(); 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)> { fn enumerate_processes() -> Vec<(u32, String)> {
use windows_sys::Win32::System::ProcessStatus::{EnumProcesses, GetModuleBaseNameW}; use windows_sys::Win32::System::ProcessStatus::{EnumProcesses, GetModuleBaseNameW};
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};

@ -1,6 +1,10 @@
pub const FLASH_VERSION: &str = "34.0.0.330"; pub const FLASH_VERSION: &str = "34.0.0.330";
pub const 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 struct VersionInfo {
pub name: String, pub name: String,
pub version: String, pub version: String,
@ -8,7 +12,138 @@ pub struct VersionInfo {
} }
pub fn get_latest_version() -> Option<VersionInfo> { pub fn get_latest_version() -> Option<VersionInfo> {
// The original fetches from the GitHub API. let json = fetch_https(API_HOST, API_PATH, USER_AGENT)?;
// Stubbed for the port; real implementation would use ureq or reqwest. let name = extract_json_string(&json, "name")?;
None 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<String> {
use windows_sys::Win32::Networking::WinHttp::{
WinHttpCloseHandle, WinHttpConnect, WinHttpOpen, WinHttpOpenRequest,
WinHttpQueryDataAvailable, WinHttpReadData, WinHttpReceiveResponse,
WinHttpSendRequest, WINHTTP_FLAG_SECURE,
};
let wide = |s: &str| -> Vec<u16> { 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<u8> = 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<String> {
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)
}

@ -30,6 +30,12 @@ fn main() {
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application> </application>
</compatibility> </compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly> </assembly>
"#); "#);
let _ = res.compile(); let _ = res.compile();

@ -54,6 +54,7 @@ pub struct ProgressState {
/// Full application state for the installer form. /// Full application state for the installer form.
pub struct InstallForm { pub struct InstallForm {
scale: f32,
pub panel: Panel, pub panel: Panel,
// Header // Header
pub title_text: String, pub title_text: String,
@ -109,7 +110,24 @@ pub struct InstallForm {
} }
impl 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 version = update_checker::FLASH_VERSION;
let title_text = "Clean Flash Player".to_string(); let title_text = "Clean Flash Player".to_string();
let subtitle_text = format!("built from version {} (China)", version); let subtitle_text = format!("built from version {} (China)", version);
@ -123,102 +141,63 @@ impl InstallForm {
let fonts = FontManager::new(); let fonts = FontManager::new();
Self { Self {
scale,
panel: Panel::Disclaimer, panel: Panel::Disclaimer,
title_text, title_text,
subtitle_text, subtitle_text,
flash_logo, flash_logo,
checkbox_on, checkbox_on,
checkbox_off, checkbox_off,
prev_button: GradientButton::new(90, 286, 138, 31, "QUIT"), prev_button: btn(90, 286, 138, 31, "QUIT"),
next_button: GradientButton::new(497, 286, 138, 31, "AGREE"), next_button: btn(497, 286, 138, 31, "AGREE"),
// Disclaimer panel // Disclaimer panel
disclaimer_label: Label::new(PANEL_X + 25, PANEL_Y, DISCLAIMER_TEXT, 13.0), disclaimer_label: lbl(PANEL_X + 25, PANEL_Y, DISCLAIMER_TEXT, 15.0),
disclaimer_box: ImageCheckBox::new(PANEL_X, PANEL_Y), disclaimer_box: chk(PANEL_X, PANEL_Y),
// Choice panel // Choice panel
browser_ask_label: Label::new( browser_ask_label: lbl(PANEL_X - 2, PANEL_Y + 2, "Which browser plugins would you like to install?", 15.0),
PANEL_X - 2, pepper_box: chk(PANEL_X, PANEL_Y + 47),
PANEL_Y + 2, pepper_label: lbl(PANEL_X + 24, PANEL_Y + 47, "Pepper API (PPAPI)\n(Chrome/Opera/Brave)", 15.0),
"Which browser plugins would you like to install?", netscape_box: chk(PANEL_X + 186, PANEL_Y + 47),
13.0, 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),
pepper_box: ImageCheckBox::new(PANEL_X, PANEL_Y + 47), activex_label: lbl(PANEL_X + 389, PANEL_Y + 47, "ActiveX (OCX)\n(IE/Embedded/Desktop)", 15.0),
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,
),
// Player choice panel // Player choice panel
player_ask_label: Label::new( player_ask_label: lbl(PANEL_X - 2, PANEL_Y + 2, "Would you like to install the standalone Flash Player?", 15.0),
PANEL_X - 2, player_box: chk(PANEL_X, PANEL_Y + 47),
PANEL_Y + 2, player_label: lbl(PANEL_X + 24, PANEL_Y + 47, "Install Standalone\nFlash Player", 15.0),
"Would you like to install the standalone Flash Player?", player_desktop_box: chk(PANEL_X + 186, PANEL_Y + 47),
13.0, 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_box: ImageCheckBox::new(PANEL_X, PANEL_Y + 47), player_start_menu_label: lbl(PANEL_X + 389, PANEL_Y + 47, "Create Shortcuts\nin Start Menu", 15.0),
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,
),
// Debug choice panel // Debug choice panel
debug_ask_label: Label::new( debug_ask_label: lbl(
PANEL_X - 2, PANEL_X - 2,
PANEL_Y + 2, PANEL_Y + 2,
"Would you like to install the debug version of Clean Flash Player?\n\ "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\ You should only choose the debug version if you are planning to create Flash applications.\n\
If you are not sure, simply press NEXT.", 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, debug_chosen: false,
// Before install panel // 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 panel
install_header_label: Label::new(PANEL_X + 3, PANEL_Y, "Installation in progress...", 13.0), install_header_label: lbl(PANEL_X + 3, PANEL_Y, "Installation in progress...", 15.0),
progress_label: Label::new(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 13.0), progress_label: lbl(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 15.0),
progress_bar: ProgressBar::new(PANEL_X + 49, PANEL_Y + 58, 451, 23), progress_bar: ProgressBar::new(s(PANEL_X + 49), s(PANEL_Y + 58), s(451), s(23)),
// Complete panel // Complete panel
complete_label: Label::new(PANEL_X, PANEL_Y, "", 13.0), complete_label: lbl(PANEL_X, PANEL_Y, "", 15.0),
// Failure panel // Failure panel
failure_text_label: Label::new( failure_text_label: lbl(
PANEL_X + 3, PANEL_X + 3,
PANEL_Y + 2, PANEL_Y + 2,
"Oops! The installation process has encountered an unexpected problem.\n\ "Oops! The installation process has encountered an unexpected problem.\n\
The following details could be useful. Press the Retry button to try again.", The following details could be useful. Press the Retry button to try again.",
13.0, 15.0,
), ),
failure_detail: String::new(), 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 { progress_state: Arc::new(Mutex::new(ProgressState {
label: "Preparing...".into(), label: "Preparing...".into(),
value: 0, 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. /// Called each frame: handle input, update state, draw.
pub fn update_and_draw( pub fn update_and_draw(
&mut self, &mut self,
@ -257,30 +241,32 @@ The following details could be useful. Press the Retry button to try again.",
renderer.clear(BG_COLOR); renderer.clear(BG_COLOR);
// Header: flash logo. // 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. // Title.
self.fonts.draw_text( self.fonts.draw_text(
renderer, renderer,
233, self.s(233),
54, self.s(54),
&self.title_text, &self.title_text,
32.0, // ~24pt Segoe UI self.sf(32.0),
FG_COLOR, FG_COLOR,
); );
// Subtitle. // Subtitle.
self.fonts.draw_text( self.fonts.draw_text(
renderer, renderer,
280, self.s(280),
99, self.s(99),
&self.subtitle_text, &self.subtitle_text,
17.0, // ~13pt Segoe UI self.sf(17.0),
FG_COLOR, FG_COLOR,
); );
// Separator line at y=270. // Separator line.
renderer.fill_rect(0, 270, WIDTH as i32, 1, Renderer::rgb(105, 105, 105)); renderer.fill_rect(0, self.s(270), renderer.width as i32, self.s(1).max(1), Renderer::rgb(105, 105, 105));
// Draw current panel. // Draw current panel.
match self.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) { fn draw_failure(&self, r: &mut Renderer) {
self.failure_text_label.draw(r, &self.fonts); self.failure_text_label.draw(r, &self.fonts);
// Draw error detail as clipped text. // Draw error detail as clipped text.
let detail_y = PANEL_Y + 44;
let detail_text = if self.failure_detail.len() > 300 { let detail_text = if self.failure_detail.len() > 300 {
&self.failure_detail[..300] &self.failure_detail[..300]
} else { } else {
@ -636,12 +621,12 @@ including Clean Flash Player and older versions of Adobe Flash Player."
}; };
self.fonts.draw_text_multiline( self.fonts.draw_text_multiline(
r, r,
PANEL_X + 4, self.s(PANEL_X + 4),
detail_y, self.s(PANEL_Y + 44),
detail_text, detail_text,
11.0, self.sf(11.0),
FG_COLOR, FG_COLOR,
1.0, self.sf(1.0),
); );
self.copy_error_button.draw(r, &self.fonts); self.copy_error_button.draw(r, &self.fonts);
} }

@ -1,4 +1,4 @@
#![windows_subsystem = "windows"] //#![windows_subsystem = "windows"]
mod install_flags; mod install_flags;
mod install_form; mod install_form;
@ -14,10 +14,16 @@ fn main() {
clean_flash_common::update_checker::FLASH_VERSION 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( let mut window = Window::new(
&title, &title,
WIDTH, phys_w,
HEIGHT, phys_h,
WindowOptions { WindowOptions {
resize: false, resize: false,
..WindowOptions::default() ..WindowOptions::default()
@ -31,8 +37,9 @@ fn main() {
// Cap at ~60 fps. // Cap at ~60 fps.
window.set_target_fps(60); window.set_target_fps(60);
let mut renderer = Renderer::new(WIDTH, HEIGHT); // Renderer operates at physical resolution; the form layout is scaled accordingly.
let mut form = InstallForm::new(); 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) { while window.is_open() && !window.is_key_down(Key::Escape) {
let (mx, my) = window let (mx, my) = window
@ -43,7 +50,7 @@ fn main() {
form.update_and_draw(&mut renderer, mx as i32, my as i32, mouse_down); form.update_and_draw(&mut renderer, mx as i32, my as i32, mouse_down);
window window
.update_with_buffer(&renderer.buffer, WIDTH, HEIGHT) .update_with_buffer(&renderer.buffer, phys_w, phys_h)
.expect("Failed to update window buffer"); .expect("Failed to update window buffer");
} }
} }

@ -5,6 +5,20 @@ pub mod widgets;
pub use font::FontManager; pub use font::FontManager;
pub use renderer::Renderer; 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 /// Set the window icon from the icon resource already embedded in the binary
/// by `winresource` (resource ID 1). No-op on non-Windows platforms. /// by `winresource` (resource ID 1). No-op on non-Windows platforms.
pub fn set_window_icon(window: &minifb::Window) { pub fn set_window_icon(window: &minifb::Window) {

@ -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). /// Draw an RGBA image onto the framebuffer at (x, y).
pub fn draw_image(&mut self, x: i32, y: i32, img: &RgbaImage) { pub fn draw_image(&mut self, x: i32, y: i32, img: &RgbaImage) {
for iy in 0..img.height as i32 { for iy in 0..img.height as i32 {

@ -6,6 +6,7 @@ use crate::renderer::Renderer;
pub struct GradientButton { pub struct GradientButton {
pub rect: Rect, pub rect: Rect,
pub text: String, pub text: String,
pub font_size: f32,
pub color1: u32, pub color1: u32,
pub color2: u32, pub color2: u32,
pub back_color: u32, pub back_color: u32,
@ -23,6 +24,7 @@ impl GradientButton {
Self { Self {
rect: Rect::new(x, y, w, h), rect: Rect::new(x, y, w, h),
text: text.to_string(), text: text.to_string(),
font_size: 13.0,
color1: Renderer::rgb(118, 118, 118), color1: Renderer::rgb(118, 118, 118),
color2: Renderer::rgb(81, 81, 81), color2: Renderer::rgb(81, 81, 81),
back_color: Renderer::rgb(0, 0, 0), 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); renderer.draw_rect(r.x, r.y, r.w, r.h, bg);
// Measure text to centre it. // 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 (tw, th) = fonts.measure_text(&self.text, font_size);
let tx = r.x + ((r.w as f32 - tw) / 2.0) as i32; 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; let ty = r.y + ((r.h as f32 - th) / 2.0) as i32;

@ -11,8 +11,12 @@ pub struct ImageCheckBox {
impl ImageCheckBox { impl ImageCheckBox {
pub fn new(x: i32, y: i32) -> Self { 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 { Self {
rect: Rect::new(x, y, 21, 21), rect: Rect::new(x, y, size, size),
checked: true, checked: true,
enabled: true, enabled: true,
visible: true, visible: true,
@ -43,7 +47,7 @@ impl ImageCheckBox {
unchecked_img unchecked_img
}; };
if img.width > 0 && img.height > 0 { 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 { } else {
// Fallback: draw a simple square. // Fallback: draw a simple square.
let bg = if self.checked { let bg = if self.checked {

@ -8,6 +8,7 @@ pub struct Label {
pub text: String, pub text: String,
pub color: u32, pub color: u32,
pub font_size: f32, pub font_size: f32,
pub line_spacing: f32,
pub visible: bool, pub visible: bool,
} }
@ -18,6 +19,7 @@ impl Label {
text: text.to_string(), text: text.to_string(),
color: Renderer::rgb(245, 245, 245), color: Renderer::rgb(245, 245, 245),
font_size, font_size,
line_spacing: 2.0,
visible: true, visible: true,
} }
} }
@ -33,7 +35,7 @@ impl Label {
&self.text, &self.text,
self.font_size, self.font_size,
self.color, self.color,
2.0, self.line_spacing,
); );
} }

@ -28,6 +28,12 @@ fn main() {
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application> </application>
</compatibility> </compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly> </assembly>
"#); "#);
let _ = res.compile(); let _ = res.compile();

@ -12,10 +12,14 @@ fn main() {
clean_flash_common::update_checker::FLASH_VERSION 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( let mut window = Window::new(
&title, &title,
WIDTH, phys_w,
HEIGHT, phys_h,
WindowOptions { WindowOptions {
resize: false, resize: false,
..WindowOptions::default() ..WindowOptions::default()
@ -28,8 +32,8 @@ fn main() {
window.set_target_fps(60); window.set_target_fps(60);
let mut renderer = Renderer::new(WIDTH, HEIGHT); let mut renderer = Renderer::new(phys_w, phys_h);
let mut form = UninstallForm::new(); let mut form = UninstallForm::new(scale);
while window.is_open() && !window.is_key_down(Key::Escape) { while window.is_open() && !window.is_key_down(Key::Escape) {
let (mx, my) = window let (mx, my) = window
@ -40,7 +44,7 @@ fn main() {
form.update_and_draw(&mut renderer, mx as i32, my as i32, mouse_down); form.update_and_draw(&mut renderer, mx as i32, my as i32, mouse_down);
window window
.update_with_buffer(&renderer.buffer, WIDTH, HEIGHT) .update_with_buffer(&renderer.buffer, phys_w, phys_h)
.expect("Failed to update window buffer"); .expect("Failed to update window buffer");
} }
} }

@ -39,6 +39,7 @@ struct ProgressState {
} }
pub struct UninstallForm { pub struct UninstallForm {
scale: f32,
panel: Panel, panel: Panel,
title_text: String, title_text: String,
subtitle_text: String, subtitle_text: String,
@ -64,37 +65,45 @@ pub struct UninstallForm {
} }
impl 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 version = update_checker::FLASH_VERSION;
let flash_logo = load_resource_image("flashLogo.png"); let flash_logo = load_resource_image("flashLogo.png");
let fonts = FontManager::new(); let fonts = FontManager::new();
Self { Self {
scale,
panel: Panel::BeforeInstall, panel: Panel::BeforeInstall,
title_text: "Clean Flash Player".into(), title_text: "Clean Flash Player".into(),
subtitle_text: format!("built from version {} (China)", version), subtitle_text: format!("built from version {} (China)", version),
flash_logo, flash_logo,
prev_button: GradientButton::new(90, 286, 138, 31, "QUIT"), prev_button: btn(90, 286, 138, 31, "QUIT"),
next_button: GradientButton::new(497, 286, 138, 31, "UNINSTALL"), next_button: btn(497, 286, 138, 31, "UNINSTALL"),
before_label: Label::new(PANEL_X + 3, PANEL_Y + 2, BEFORE_TEXT, 13.0), before_label: lbl(PANEL_X + 3, PANEL_Y + 2, BEFORE_TEXT, 15.0),
progress_header: Label::new( progress_header: lbl(PANEL_X + 3, PANEL_Y, "Uninstallation in progress...", 15.0),
PANEL_X + 3, progress_label: lbl(PANEL_X + 46, PANEL_Y + 30, "Preparing...", 15.0),
PANEL_Y, progress_bar: ProgressBar::new(s(PANEL_X + 49), s(PANEL_Y + 58), s(451), s(23)),
"Uninstallation in progress...", complete_label: lbl(PANEL_X, PANEL_Y, COMPLETE_TEXT, 15.0),
13.0, failure_text_label: lbl(
),
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(
PANEL_X + 3, PANEL_X + 3,
PANEL_Y + 2, PANEL_Y + 2,
"Oops! The installation process has encountered an unexpected problem.\n\ "Oops! The installation process has encountered an unexpected problem.\n\
The following details could be useful. Press the Retry button to try again.", The following details could be useful. Press the Retry button to try again.",
13.0, 15.0,
), ),
failure_detail: String::new(), 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 { progress_state: Arc::new(Mutex::new(ProgressState {
label: "Preparing...".into(), label: "Preparing...".into(),
value: 0, value: 0,
@ -147,15 +156,17 @@ The following details could be useful. Press the Retry button to try again.",
// Draw. // Draw.
renderer.clear(BG_COLOR); 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 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 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. // 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 { match self.panel {
Panel::BeforeInstall => self.before_label.draw(renderer, &self.fonts), 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( self.fonts.draw_text_multiline(
renderer, renderer,
PANEL_X + 4, self.s(PANEL_X + 4),
PANEL_Y + 44, self.s(PANEL_Y + 44),
detail, detail,
11.0, self.sf(11.0),
FG_COLOR, FG_COLOR,
1.0, self.sf(1.0),
); );
self.copy_error_button.draw(renderer, &self.fonts); 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); 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) { fn start_uninstall(&mut self) {
self.panel = Panel::Install; self.panel = Panel::Install;
self.prev_button.enabled = false; self.prev_button.enabled = false;

Loading…
Cancel
Save