use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use std::sync::Mutex; use std::thread; use std::time::Duration; use tauri::Emitter; static OVERLAY_ACTIVE: AtomicBool = AtomicBool::new(false); static OVERLAY_HANDLE: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); static DRAG_MODE: AtomicBool = AtomicBool::new(false); static POSITION_CHANGED: AtomicBool = AtomicBool::new(false); static BACKGROUND_POLLER_ACTIVE: AtomicBool = AtomicBool::new(false); #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct DisplayItem { pub id: String, pub label: String, pub enabled: bool, } pub type DisplayItems = Vec; fn default_style() -> String { "default".to_string() } fn default_font() -> String { "Microsoft YaHei".to_string() } fn default_font_color() -> String { "#ffffff".to_string() } fn default_display_items() -> DisplayItems { vec![ DisplayItem { id: "fps".to_string(), label: "FPS".to_string(), enabled: false }, DisplayItem { id: "cpu_temp".to_string(), label: "CPU温度".to_string(), enabled: false }, DisplayItem { id: "cpu_usage".to_string(), label: "CPU占用".to_string(), enabled: true }, DisplayItem { id: "cpu_fan_speed".to_string(), label: "CPU风扇转速".to_string(), enabled: false }, DisplayItem { id: "cpu_clock".to_string(), label: "CPU频率".to_string(), enabled: false }, DisplayItem { id: "cpu_voltage".to_string(), label: "CPU电压".to_string(), enabled: false }, DisplayItem { id: "cpu_power".to_string(), label: "CPU功耗".to_string(), enabled: false }, DisplayItem { id: "gpu_temp".to_string(), label: "GPU温度".to_string(), enabled: true }, DisplayItem { id: "gpu_usage".to_string(), label: "GPU占用".to_string(), enabled: true }, DisplayItem { id: "gpu_fan_speed".to_string(), label: "GPU风扇转速".to_string(), enabled: false }, DisplayItem { id: "gpu_power".to_string(), label: "GPU功耗".to_string(), enabled: false }, DisplayItem { id: "gpu_clock".to_string(), label: "GPU频率".to_string(), enabled: false }, DisplayItem { id: "gpu_voltage".to_string(), label: "GPU电压".to_string(), enabled: false }, DisplayItem { id: "gpu_vram".to_string(), label: "GPU显存占用".to_string(), enabled: false }, DisplayItem { id: "gpu_memory_clock".to_string(), label: "GPU显存频率".to_string(), enabled: false }, DisplayItem { id: "memory_usage".to_string(), label: "内存占用".to_string(), enabled: true }, DisplayItem { id: "ssd_temp".to_string(), label: "硬盘温度".to_string(), enabled: false }, DisplayItem { id: "game_ping".to_string(), label: "游戏延迟".to_string(), enabled: true }, DisplayItem { id: "delta_password".to_string(), label: "三角洲密码".to_string(), enabled: true }, ] } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct CustomOverlayItem { pub id: String, pub text: String, pub color: String, #[serde(default)] pub enabled: bool, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct OverlaySettings { #[serde(default = "default_display_items")] pub display_items: DisplayItems, #[serde(default)] pub custom_items: Vec, pub opacity: u8, #[serde(default = "default_style")] pub style: String, #[serde(default = "default_font")] pub font: String, #[serde(default = "default_font_color")] pub font_color: String, #[serde(default)] pub position_x: Option, #[serde(default)] pub position_y: Option, /// 三角洲密码选中的地图名称列表(空 = 显示全部) #[serde(default)] pub delta_password_maps: Vec, } impl Default for OverlaySettings { fn default() -> Self { Self { display_items: default_display_items(), custom_items: Vec::new(), opacity: 255, style: "default".to_string(), font: "Microsoft YaHei".to_string(), font_color: "#ffffff".to_string(), position_x: None, position_y: None, delta_password_maps: Vec::new(), } } } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct OverlayResult { pub success: bool, pub message: String, } #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct OverlayHardwareData { fps: Option, cpu_usage: Option, cpu_temp: Option, cpu_clock: Option, gpu_temp: Option, gpu_usage: Option, memory_usage: Option, delta_password: Option, game_ping: Option, cpu_fan_speed: Option, gpu_fan_speed: Option, gpu_power: Option, gpu_clock: Option, gpu_vram_used: Option, gpu_vram_total: Option, gpu_memory_clock: Option, cpu_voltage: Option, gpu_voltage: Option, cpu_power: Option, ssd_temp: Option, } impl Default for OverlayHardwareData { fn default() -> Self { Self { fps: None, cpu_usage: None, cpu_temp: None, cpu_clock: None, gpu_temp: None, gpu_usage: None, memory_usage: None, delta_password: None, game_ping: None, cpu_fan_speed: None, gpu_fan_speed: None, gpu_power: None, gpu_clock: None, gpu_vram_used: None, gpu_vram_total: None, gpu_memory_clock: None, cpu_voltage: None, gpu_voltage: None, cpu_power: None, ssd_temp: None, } } } pub static CURRENT_SETTINGS: Mutex> = Mutex::new(None); pub static CURRENT_HARDWARE_DATA: Mutex> = Mutex::new(None); pub fn get_or_init_settings() -> OverlaySettings { let mut settings_lock = CURRENT_SETTINGS.lock().unwrap(); if settings_lock.is_none() { *settings_lock = Some(OverlaySettings::default()); } settings_lock.as_ref().unwrap().clone() } /// 从 LHML 传感器列表中提取指定类型的传感器值(多模式匹配) fn extract_sensor( sensors: &[crate::sensor::SensorReading], sensor_type: &str, hardware_type: &str, names: &[&str], skip_zero: bool, ) -> Option<(f64, String)> { for name in names { if let Some(s) = sensors.iter().find(|s| { s.sensor_type == sensor_type && s.hardware_type.eq_ignore_ascii_case(hardware_type) && s.name.contains(name) && (!skip_zero || s.value > 0.0) }) { return Some((s.value, s.name.clone())); } } None } /// 从 LHML 传感器列表中提取所有匹配传感器值的平均值(用于风扇转速等) fn extract_all_avg( sensors: &[crate::sensor::SensorReading], sensor_type: &str, hardware_type: &str, name_prefixes: &[&str], ) -> Option { let values: Vec = sensors .iter() .filter(|s| { s.sensor_type == sensor_type && s.hardware_type.eq_ignore_ascii_case(hardware_type) && name_prefixes.iter().any(|p| s.name.starts_with(p)) }) .map(|s| s.value) .collect(); if values.is_empty() { None } else { Some(values.iter().sum::() / values.len() as f64) } } static LAST_LHML_UPDATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); pub fn collect_hardware_data() -> OverlayHardwareData { let fps = crate::game_fps::get_cached_fps(); let selected_maps = { let settings = get_or_init_settings(); settings.delta_password_maps.clone() }; let delta_password = if selected_maps.is_empty() { crate::delta_force::get_cached_delta_password() } else { crate::delta_force::get_cached_delta_password_filtered(&selected_maps) }; let game_ping = crate::game_ping::get_cached_ping(); let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; let last_time = LAST_LHML_UPDATE.load(std::sync::atomic::Ordering::Relaxed); let use_cached_lhml = if current_time - last_time < 1000 { true } else { LAST_LHML_UPDATE.store(current_time, std::sync::atomic::Ordering::Relaxed); false }; // 从 LHML (NexBoxMonitor) 获取硬件传感器数据 let (cpu_usage, cpu_temp, cpu_clock, cpu_voltage, cpu_power, cpu_fan_speed, ssd_temp, memory_usage, gpu_temp, gpu_usage, gpu_fan_speed, gpu_power, gpu_clock, gpu_vram_used, gpu_vram_total, gpu_memory_clock, gpu_voltage) = if use_cached_lhml { let prev = CURRENT_HARDWARE_DATA.lock().unwrap().clone().unwrap_or_default(); ( prev.cpu_usage, prev.cpu_temp, prev.cpu_clock, prev.cpu_voltage, prev.cpu_power, prev.cpu_fan_speed, prev.ssd_temp, prev.memory_usage, prev.gpu_temp, prev.gpu_usage, prev.gpu_fan_speed, prev.gpu_power, prev.gpu_clock, prev.gpu_vram_used, prev.gpu_vram_total, prev.gpu_memory_clock, prev.gpu_voltage ) } else { match crate::sensor::read_lhm_sensors() { Ok(response) => { // CPU 占用 (Load 类型) let (cpu_usage, cpu_usage_name) = extract_sensor( &response.sensors, "Load", "CPU", &["CPU Total", "Total"], false, ).unzip(); let cpu_usage = cpu_usage.map(|v| v as u16); // CPU 温度 (AMD: Core (Tctl/Tdie), Intel: CPU Package) let (cpu_temp, cpu_name) = extract_sensor( &response.sensors, "Temperature", "CPU", &["Core (Tctl/Tdie)", "CPU Package", "Tctl", "Core"], false, ).unzip(); // CPU 频率 let (cpu_clock, cpu_clock_name) = extract_sensor( &response.sensors, "Clock", "CPU", &["Cores (Average)", "Core #1", "Bus Speed"], true, ).unzip(); let cpu_clock = cpu_clock.map(|v| v as u32); // CPU 电压 let cpu_voltage_result = extract_sensor( &response.sensors, "Voltage", "CPU", &["CPU Core", "Vcore", "Core", "CPU VCore"], false, ); let cpu_voltage = cpu_voltage_result.as_ref().map(|(v, _)| *v); // CPU 功耗 let cpu_power_result = extract_sensor( &response.sensors, "Power", "CPU", &["Package", "CPU Package", "CPU Cores", "CPU Core"], false, ); let cpu_power = cpu_power_result.as_ref().map(|(v, _)| *v); // SSD 温度 (跳过 0 值) let (ssd_temp, ssd_name) = extract_sensor( &response.sensors, "Temperature", "Storage", &["Composite Temperature", "Temperature #1", "Temperature"], true, ).unzip(); // 内存占用 (从 LHML Memory 硬件获取) let memory_usage = extract_sensor( &response.sensors, "Load", "Memory", &["Memory"], false, ).map(|(v, _)| v); // 调试:打印所有 RAM 传感器 let memory_sensors: Vec<_> = response.sensors.iter() .filter(|s| s.hardware_type.eq_ignore_ascii_case("Memory")) .map(|s| format!("{}|{}|{}={}", s.hardware_type, s.sensor_type, s.name, s.value)) .collect(); log::debug!("LHML Memory sensors: {:?}", memory_sensors); // 调试:打印所有 CPU 和主板传感器 let cpu_sensors: Vec<_> = response.sensors.iter() .filter(|s| s.hardware_type.eq_ignore_ascii_case("CPU")) .map(|s| format!("{}|{}|{}={}", s.hardware_type, s.sensor_type, s.name, s.value)) .collect(); log::debug!("LHML CPU sensors: {:?}", cpu_sensors); // 打印所有非 GPU/CPU/HDD 的硬件类型(用于排查主板传感器) let other_types: std::collections::BTreeSet<_> = response.sensors.iter() .filter(|s| !s.hardware_type.to_lowercase().starts_with("gpu") && !s.hardware_type.eq_ignore_ascii_case("CPU") && !s.hardware_type.eq_ignore_ascii_case("Storage") && !s.hardware_type.eq_ignore_ascii_case("RAM")) .map(|s| format!("{}|{}|{}={}", s.hardware_type, s.sensor_type, s.name, s.value)) .collect(); log::debug!("LHML other sensors: {:?}", other_types); // 调试:打印所有 GPU 传感器 let gpu_sensors: Vec<_> = response.sensors.iter() .filter(|s| s.hardware_type.to_lowercase().starts_with("gpu")) .map(|s| format!("{}|{}|{}={}", s.hardware_type, s.sensor_type, s.name, s.value)) .collect(); log::debug!("LHML GPU sensors: {:?}", gpu_sensors); // GPU 温度 let gpu_temp_result = extract_sensor( &response.sensors, "Temperature", "GpuNvidia", &["GPU Core"], false, ) .or_else(|| extract_sensor(&response.sensors, "Temperature", "GpuAmd", &["GPU Core"], false)) .or_else(|| extract_sensor(&response.sensors, "Temperature", "GpuIntel", &["GPU Core"], false)); let gpu_temp = gpu_temp_result.as_ref().map(|(v, _)| *v); let gpu_temp_name = gpu_temp_result.as_ref().map(|(_, n)| n.clone()); // GPU 占用 let gpu_usage_result = extract_sensor( &response.sensors, "Load", "GpuNvidia", &["GPU Core"], false, ) .or_else(|| extract_sensor(&response.sensors, "Load", "GpuAmd", &["GPU Core"], false)) .or_else(|| extract_sensor(&response.sensors, "Load", "GpuIntel", &["GPU Core"], false)); let gpu_usage = gpu_usage_result.as_ref().map(|(v, _)| *v as u32); let gpu_usage_name = gpu_usage_result.as_ref().map(|(_, n)| n.clone()); // CPU 风扇 // LHML 中 CPU 风扇传感器出现在 SuperIO(Nuvoton 等)或 Motherboard 硬件类型下 // 名称通常为 "Fan #1"、"CPU Fan"、"CPU1 Fan" 等,优先取第一个非零值风扇作为 CPU 风扇 let cpu_fan_speed = { let fan = extract_sensor(&response.sensors, "Fan", "SuperIO", &["Fan #1", "Fan #2", "CPU Fan", "CPU1 Fan", "CPUFAN"], false) .or_else(|| { // 取 SuperIO 下第一个非零 RPM 的风扇 response.sensors.iter() .filter(|s| s.sensor_type == "Fan" && s.hardware_type.eq_ignore_ascii_case("SuperIO") && s.value > 0.0) .next() .map(|s| (s.value, s.name.clone())) }) .or_else(|| { // 兜底:任意 SuperIO 风扇 response.sensors.iter() .filter(|s| s.sensor_type == "Fan" && s.hardware_type.eq_ignore_ascii_case("SuperIO")) .next() .map(|s| (s.value, s.name.clone())) }) .or_else(|| { // 再兜底:Motherboard 下的风扇 response.sensors.iter() .filter(|s| s.sensor_type == "Fan" && s.hardware_type.eq_ignore_ascii_case("Motherboard")) .next() .map(|s| (s.value, s.name.clone())) }); fan.map(|(v, _)| v as u32) }; // GPU 风扇(所有风扇平均,RPM) // 尝试多种前缀:NVIDIA 常见 "GPU Fan #0", "GPU",AMD 常见 "Fans" let gpu_fan_speed = extract_all_avg(&response.sensors, "Fan", "GpuNvidia", &["GPU Fan", "GPU"]) .or_else(|| extract_all_avg(&response.sensors, "Fan", "GpuAmd", &["GPU Fan", "GPU", "Fans"])) .map(|v| v as u32); // GPU 功耗 let gpu_power_result = extract_sensor( &response.sensors, "Power", "GpuNvidia", &["GPU Power", "GPU Package", "GPU Chip Power", "Power"], false, ) .or_else(|| { extract_sensor( &response.sensors, "Power", "GpuAmd", &["GPU Power", "GPU Package", "Power"], false, ) }) .or_else(|| { extract_sensor( &response.sensors, "Power", "GpuIntel", &["GPU Power", "GPU Package", "Power"], false, ) }); let gpu_power = gpu_power_result.as_ref().map(|(v, _)| *v as u32); let gpu_power_name = gpu_power_result.as_ref().map(|(_, n)| n.clone()); // GPU 频率 let gpu_clock_result = extract_sensor( &response.sensors, "Clock", "GpuNvidia", &["GPU Core"], false, ) .or_else(|| extract_sensor(&response.sensors, "Clock", "GpuAmd", &["GPU Core"], false)) .or_else(|| extract_sensor(&response.sensors, "Clock", "GpuIntel", &["GPU Core"], false)); let gpu_clock = gpu_clock_result.as_ref().map(|(v, _)| *v as u32); let gpu_clock_name = gpu_clock_result.as_ref().map(|(_, n)| n.clone()); // 显存占用 (MB) let gpu_vram_used_result = extract_sensor( &response.sensors, "SmallData", "GpuNvidia", &["GPU Memory Used", "D3D Shared Memory Used"], false, ) .or_else(|| extract_sensor(&response.sensors, "SmallData", "GpuAmd", &["GPU Memory Used"], false)) .or_else(|| extract_sensor(&response.sensors, "SmallData", "GpuIntel", &["GPU Memory Used"], false)); let gpu_vram_used = gpu_vram_used_result.as_ref().map(|(v, _)| *v as u32); // 显存总量 (MB) let gpu_vram_total_result = extract_sensor( &response.sensors, "SmallData", "GpuNvidia", &["GPU Memory Total"], false, ) .or_else(|| extract_sensor(&response.sensors, "SmallData", "GpuAmd", &["GPU Memory Total"], false)) .or_else(|| extract_sensor(&response.sensors, "SmallData", "GpuIntel", &["GPU Memory Total"], false)); let gpu_vram_total = gpu_vram_total_result.as_ref().map(|(v, _)| *v as u32); // 显存频率 let gpu_memory_clock_result = extract_sensor( &response.sensors, "Clock", "GpuNvidia", &["GPU Memory"], false, ) .or_else(|| extract_sensor(&response.sensors, "Clock", "GpuAmd", &["GPU Memory"], false)) .or_else(|| extract_sensor(&response.sensors, "Clock", "GpuIntel", &["GPU Memory"], false)); let gpu_memory_clock = gpu_memory_clock_result.as_ref().map(|(v, _)| *v as u32); // GPU 电压 let gpu_voltage_result = extract_sensor( &response.sensors, "Voltage", "GpuNvidia", &["GPU Core Voltage", "GPU Core"], false, ) .or_else(|| extract_sensor(&response.sensors, "Voltage", "GpuAmd", &["GPU Core Voltage", "GPU Core"], false)) .or_else(|| extract_sensor(&response.sensors, "Voltage", "GpuIntel", &["GPU Core Voltage", "GPU Core"], false)); let gpu_voltage = gpu_voltage_result.as_ref().map(|(v, _)| *v); log::debug!( "LHML: CPU占用={:?}({}) CPU温度={:?}({}) CPU频率={:?}({}) CPU电压={:?}V CPU功耗={:?}W SSD温度={:?}({}) 内存占用={:?}% GPU温度={:?}({}) GPU占用={:?}({}) GPU风扇={:?}RPM GPU功耗={:?}({}) GPU频率={:?}({}) 显存={:?}/{:?}MB 显存频率={:?}MHz GPU电压={:?}V", cpu_usage, cpu_usage_name.as_deref().unwrap_or("N/A"), cpu_temp, cpu_name.as_deref().unwrap_or("N/A"), cpu_clock, cpu_clock_name.as_deref().unwrap_or("N/A"), cpu_voltage, cpu_power, ssd_temp, ssd_name.as_deref().unwrap_or("N/A"), memory_usage, gpu_temp, gpu_temp_name.as_deref().unwrap_or("N/A"), gpu_usage, gpu_usage_name.as_deref().unwrap_or("N/A"), gpu_fan_speed, gpu_power, gpu_power_name.as_deref().unwrap_or("N/A"), gpu_clock, gpu_clock_name.as_deref().unwrap_or("N/A"), gpu_vram_used, gpu_vram_total, gpu_memory_clock, gpu_voltage, ); (cpu_usage, cpu_temp, cpu_clock, cpu_voltage, cpu_power, cpu_fan_speed, ssd_temp, memory_usage, gpu_temp, gpu_usage, gpu_fan_speed, gpu_power, gpu_clock, gpu_vram_used, gpu_vram_total, gpu_memory_clock, gpu_voltage) } Err(e) => { log::warn!("LHML 传感器读取失败: {e}"); (None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None) } } }; let new_data = OverlayHardwareData { fps, cpu_usage, cpu_temp, cpu_clock, gpu_temp, gpu_usage, memory_usage, delta_password, game_ping, cpu_fan_speed, gpu_fan_speed, gpu_power, gpu_clock, gpu_vram_used, gpu_vram_total, gpu_memory_clock, cpu_voltage, gpu_voltage, cpu_power, ssd_temp, }; let prev_data = CURRENT_HARDWARE_DATA.lock().unwrap().clone(); let result = if let Some(prev) = prev_data { OverlayHardwareData { fps: new_data.fps.or(prev.fps), cpu_usage: new_data.cpu_usage.or(prev.cpu_usage), cpu_temp: new_data.cpu_temp.or(prev.cpu_temp), cpu_clock: new_data.cpu_clock.or(prev.cpu_clock), gpu_temp: new_data.gpu_temp.or(prev.gpu_temp), gpu_usage: new_data.gpu_usage.or(prev.gpu_usage), memory_usage: new_data.memory_usage.or(prev.memory_usage), delta_password: new_data.delta_password.or_else(|| prev.delta_password.clone()), game_ping: new_data.game_ping.or(prev.game_ping), cpu_fan_speed: new_data.cpu_fan_speed.or(prev.cpu_fan_speed), gpu_fan_speed: new_data.gpu_fan_speed.or(prev.gpu_fan_speed), gpu_power: new_data.gpu_power.or(prev.gpu_power), gpu_clock: new_data.gpu_clock.or(prev.gpu_clock), gpu_vram_used: new_data.gpu_vram_used.or(prev.gpu_vram_used), gpu_vram_total: new_data.gpu_vram_total.or(prev.gpu_vram_total), gpu_memory_clock: new_data.gpu_memory_clock.or(prev.gpu_memory_clock), cpu_voltage: new_data.cpu_voltage.or(prev.cpu_voltage), gpu_voltage: new_data.gpu_voltage.or(prev.gpu_voltage), cpu_power: new_data.cpu_power.or(prev.cpu_power), ssd_temp: new_data.ssd_temp.or(prev.ssd_temp), } } else { new_data }; result } #[cfg(target_os = "windows")] mod win32 { use windows_sys::Win32::Foundation::*; use windows_sys::Win32::Graphics::Gdi::*; use windows_sys::Win32::Graphics::GdiPlus::*; use windows_sys::Win32::UI::WindowsAndMessaging::*; use windows_sys::Win32::UI::Accessibility::*; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; use std::ptr; use std::sync::Mutex; use std::result::Result::Ok; static GDIPLUS_TOKEN: Mutex> = Mutex::new(None); static WIN_EVENT_HOOK: Mutex> = Mutex::new(None); unsafe extern "system" fn win_event_proc( _h_win_event_hook: *mut std::ffi::c_void, _event: u32, hwnd: HWND, id_object: i32, _id_child: i32, _dw_event_thread: u32, _dwms_event_time: u32, ) { if id_object != 0 || hwnd.is_null() { return; } let overlay_hwnd = super::OVERLAY_HANDLE.load(std::sync::atomic::Ordering::SeqCst); if overlay_hwnd.is_null() { return; } if hwnd != overlay_hwnd { SetWindowPos( overlay_hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, ); } } pub unsafe fn install_topmost_guard() { let hook = SetWinEventHook( EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, std::ptr::null_mut(), Some(win_event_proc), 0, 0, WINEVENT_OUTOFCONTEXT, ); if !hook.is_null() { let mut lock = WIN_EVENT_HOOK.lock().unwrap(); *lock = Some(hook as usize); } } pub unsafe fn uninstall_topmost_guard() { let mut lock = WIN_EVENT_HOOK.lock().unwrap(); if let Some(hook) = lock.take() { UnhookWinEvent(hook as *mut std::ffi::c_void); } } pub unsafe fn init_gdiplus() -> bool { let mut token = GDIPLUS_TOKEN.lock().unwrap(); if token.is_some() { return true; } let mut input = GdiplusStartupInput { GdiplusVersion: 1, DebugEventCallback: 0, SuppressBackgroundThread: 0, SuppressExternalCodecs: 0, }; let mut token_value: usize = 0; let result = GdiplusStartup(&mut token_value, &mut input, ptr::null_mut()); if result == 0 { *token = Some(token_value); true } else { log::error!("GDI+ 初始化失败: {}", result); false } } pub unsafe fn shutdown_gdiplus() { let mut token = GDIPLUS_TOKEN.lock().unwrap(); if let Some(t) = token.take() { GdiplusShutdown(t); } } fn calculate_window_width(settings: &super::OverlaySettings) -> i32 { // 使用较小的单项宽度以缩小悬浮框 let normal_item_width = 130; // 默认密码项宽度(逻辑像素) let mut password_item_width = 220; if settings.display_items.iter().any(|item| item.id == "delta_password" && item.enabled) { if let Ok(lock) = super::CURRENT_HARDWARE_DATA.lock() { if let Some(ref data) = *lock { if let Some(ref pwd) = data.delta_password { unsafe { // 使用屏幕 DC 和字体测量文本宽度,按 DPI 缩放 let screen_dc = GetDC(ptr::null_mut()); if !screen_dc.is_null() { let dpi_x = GetDeviceCaps(screen_dc, 88); let dpi_scale = dpi_x as f32 / 96.0; let hfont = create_compatible_font(dpi_scale, &settings.font); if !hfont.is_null() { let val_w = measure_text_width(screen_dc, hfont, pwd); let est = val_w + (12.0 * dpi_scale) as i32 + 20; if est > password_item_width { password_item_width = est; } DeleteObject(hfont as _); } ReleaseDC(ptr::null_mut(), screen_dc); } } } } } } let mut width = 0i32; let mut enabled_count = 0i32; for item in &settings.display_items { if item.enabled { enabled_count += 1; match item.id.as_str() { "delta_password" => { width += password_item_width; } _ => { width += normal_item_width; } } } } // 自定义项宽度(各 150px 基础宽度) let custom_item_width = 150; let mut custom_count = 0i32; for custom in &settings.custom_items { if custom.enabled && !custom.text.is_empty() { width += custom_item_width; custom_count += 1; } } if width == 0 { return 200; } enabled_count += custom_count; let sep_count = if enabled_count > 1 { enabled_count - 1 } else { 0 }; width + 32 + sep_count * 16 } pub unsafe fn create_overlay_window( settings: &super::OverlaySettings, ) -> Result { init_gdiplus(); let h_instance = GetModuleHandleW(ptr::null()); if h_instance.is_null() { return Err("无法获取模块句柄".to_string()); } let class_name = windows_sys::core::w!("NexBoxOverlayPanel"); let wnd_class = WNDCLASSW { style: CS_HREDRAW | CS_VREDRAW, lpfnWndProc: Some(window_proc), cbClsExtra: 0, cbWndExtra: 0, hInstance: h_instance, hIcon: LoadIconW(h_instance, IDI_APPLICATION), hCursor: LoadCursorW(h_instance, IDC_ARROW), hbrBackground: CreateSolidBrush(0), lpszMenuName: ptr::null(), lpszClassName: class_name, }; if RegisterClassW(&wnd_class) == 0 { let error = GetLastError(); if error != 1410 { return Err(format!("注册窗口类失败: {}", error)); } } let screen_dc = GetDC(ptr::null_mut()); let dpi_x = if screen_dc.is_null() { 96 } else { GetDeviceCaps(screen_dc, 88) }; if !screen_dc.is_null() { ReleaseDC(ptr::null_mut(), screen_dc); } let dpi_scale = dpi_x as f32 / 96.0; let logical_width = calculate_window_width(settings); let logical_height = if settings.style == "dynamic_island" { 36 } else { 28 }; let physical_width = (logical_width as f32 * dpi_scale) as i32; let physical_height = (logical_height as f32 * dpi_scale) as i32; // 使用保存的位置,或使用默认位置 let (x, y) = if let (Some(px), Some(py)) = (settings.position_x, settings.position_y) { (px, py) } else { let screen_width = GetSystemMetrics(SM_CXSCREEN); let default_x = (screen_width - physical_width) / 2; let default_y = if settings.style == "dynamic_island" { 4 } else { 0 }; (default_x, default_y) }; let hwnd = CreateWindowExW( WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT, class_name, windows_sys::core::w!("NexBox Overlay Panel"), WS_POPUP, x, y, physical_width, physical_height, ptr::null_mut(), ptr::null_mut(), h_instance, ptr::null_mut(), ); if hwnd.is_null() { return Err("创建窗口失败".to_string()); } ShowWindow(hwnd, SW_SHOW); UpdateWindow(hwnd); Ok(hwnd) } pub unsafe fn destroy_overlay_window(hwnd: HWND) -> bool { if hwnd.is_null() { return false; } KillTimer(hwnd, 1); DestroyWindow(hwnd) != 0 } unsafe fn create_compatible_font(dpi_scale: f32, font_name: &str) -> HFONT { let font_height = -(13.0 * dpi_scale).round() as i32; let wide_name: Vec = font_name.encode_utf16().chain(std::iter::once(0)).collect(); CreateFontW( font_height, 0, 0, 0, FW_NORMAL as i32, 0, 0, 0, DEFAULT_CHARSET as u32, OUT_DEFAULT_PRECIS as u32, CLIP_DEFAULT_PRECIS as u32, CLEARTYPE_QUALITY as u32, (DEFAULT_PITCH | FF_DONTCARE) as u32, wide_name.as_ptr(), ) } unsafe fn measure_text_width(hdc: HDC, hfont: HFONT, text: &str) -> i32 { let old_font = SelectObject(hdc, hfont as _); let wide: Vec = text.encode_utf16().chain(std::iter::once(0)).collect(); let mut size = SIZE { cx: 0, cy: 0 }; GetTextExtentPoint32W(hdc, wide.as_ptr(), (wide.len() - 1) as i32, &mut size); SelectObject(hdc, old_font); size.cx } struct DisplayItem { label: String, value: String, label_width: i32, value_width: i32, total_width: i32, custom_color: Option, } fn parse_hex_color(hex: &str) -> u32 { let hex = hex.trim_start_matches('#'); if let Ok(val) = u32::from_str_radix(hex, 16) { // 前端使用 #RRGGBB 格式,GDI+ 颜色格式为 0x00BBGGRR let r = (val >> 16) & 0xFF; let g = (val >> 8) & 0xFF; let b = val & 0xFF; (b << 16) | (g << 8) | r } else { 0x00FFFFFF } } fn build_display_items( settings: &super::OverlaySettings, data: &super::OverlayHardwareData, ) -> Vec { let mut items = Vec::new(); for display_item in &settings.display_items { if !display_item.enabled { continue; } match display_item.id.as_str() { "cpu_usage" => { let val = data.cpu_usage.map(|v| format!("{}%", v)).unwrap_or_else(|| "--%".to_string()); items.push(DisplayItem { label: "CPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_temp" => { let val = data.gpu_temp.map(|v| format!("{:.0}°C", v)).unwrap_or_else(|| "--°C".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_usage" => { let val = data.gpu_usage.map(|v| format!("{}%", v)).unwrap_or_else(|| "--%".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "memory_usage" => { let val = data.memory_usage.map(|v| format!("{}%", v.round() as i32)).unwrap_or_else(|| "--%".to_string()); items.push(DisplayItem { label: "RAM".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "delta_password" => { let val = data.delta_password.as_deref().unwrap_or("--").to_string(); items.push(DisplayItem { label: "".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "game_ping" => { let val = data.game_ping.map(|v| format!("{}ms", v)).unwrap_or_else(|| "--ms".to_string()); items.push(DisplayItem { label: "PING".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "fps" => { let (val, color) = match data.fps { Some(v) => { let c = if v < 30 { 0x000000FFu32 } else if v < 60 { 0x0000FFFFu32 } else { 0x0000FF00u32 }; (format!("{}", v), Some(c)) } None => ("--".to_string(), None), }; items.push(DisplayItem { label: "FPS".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: color }); } "cpu_fan_speed" => { let val = data.cpu_fan_speed.map(|v| format!("{}RPM", v)).unwrap_or_else(|| "--RPM".to_string()); items.push(DisplayItem { label: "CPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_fan_speed" => { let val = data.gpu_fan_speed.map(|v| format!("{}RPM", v)).unwrap_or_else(|| "--RPM".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_power" => { let val = data.gpu_power.map(|v| format!("{}W", v)).unwrap_or_else(|| "--W".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_clock" => { let val = data.gpu_clock.map(|v| format!("{}MHz", v)).unwrap_or_else(|| "--MHz".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_vram" => { let val = match (data.gpu_vram_used, data.gpu_vram_total) { (Some(used), Some(total)) => { let used_gb = used as f64 / 1024.0; let total_gb = total as f64 / 1024.0; format!("{:.1}G/{:.1}G", used_gb, total_gb) } _ => "--G/--G".to_string(), }; items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_memory_clock" => { let val = data.gpu_memory_clock.map(|v| format!("{}MHz", v)).unwrap_or_else(|| "--MHz".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "gpu_voltage" => { let val = data.gpu_voltage.map(|v| format!("{:.3}V", v)).unwrap_or_else(|| "--V".to_string()); items.push(DisplayItem { label: "GPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "cpu_temp" => { let val = data.cpu_temp.map(|v| format!("{:.0}°C", v)).unwrap_or_else(|| "--°C".to_string()); items.push(DisplayItem { label: "CPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "cpu_clock" => { let val = data.cpu_clock.map(|v| format!("{}MHz", v)).unwrap_or_else(|| "--MHz".to_string()); items.push(DisplayItem { label: "CPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "cpu_voltage" => { let val = data.cpu_voltage.map(|v| format!("{:.3}V", v)).unwrap_or_else(|| "--V".to_string()); items.push(DisplayItem { label: "CPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "cpu_power" => { let val = data.cpu_power.map(|v| format!("{:.1}W", v)).unwrap_or_else(|| "--W".to_string()); items.push(DisplayItem { label: "CPU".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } "ssd_temp" => { let val = data.ssd_temp.map(|v| format!("{:.0}°C", v)).unwrap_or_else(|| "--°C".to_string()); items.push(DisplayItem { label: "硬盘".to_string(), value: val, label_width: 0, value_width: 0, total_width: 0, custom_color: None }); } _ => {} } } for custom in &settings.custom_items { if custom.enabled && !custom.text.is_empty() { let color = parse_hex_color(&custom.color); items.push(DisplayItem { label: String::new(), value: custom.text.clone(), label_width: 0, value_width: 0, total_width: 0, custom_color: Some(color), }); } } items } unsafe fn measure_and_layout_items( hdc: HDC, hfont: HFONT, items: &mut [DisplayItem], dpi_scale: f32, ) -> i32 { let gap = (10.0 * dpi_scale) as i32; let mut total = 0i32; for item in items.iter_mut() { item.label_width = measure_text_width(hdc, hfont, &item.label); item.value_width = measure_text_width(hdc, hfont, &item.value); if item.label.is_empty() { item.total_width = item.value_width; } else { item.total_width = item.label_width + gap + item.value_width; } total += item.total_width; } total } pub unsafe fn draw_overlay_content( hwnd: HWND, settings: &super::OverlaySettings, data: &super::OverlayHardwareData, ) { let dpi_scale = { let dc = GetDC(hwnd); let dpi = if dc.is_null() { 96 } else { GetDeviceCaps(dc, 88) }; if !dc.is_null() { ReleaseDC(hwnd, dc); } dpi as f32 / 96.0 }; let hfont = create_compatible_font(dpi_scale, &settings.font); if hfont.is_null() { return; } let font_color = parse_hex_color(&settings.font_color); let temp_dc = GetDC(ptr::null_mut()); let mut items = build_display_items(settings, data); let padding = (16.0 * dpi_scale) as i32; let item_gap = (16.0 * dpi_scale) as i32; let content_width = measure_and_layout_items(temp_dc, hfont, &mut items, dpi_scale); ReleaseDC(ptr::null_mut(), temp_dc); let sep_count = if items.len() > 1 { items.len() as i32 - 1 } else { 0 }; let total_content_width = content_width + sep_count * item_gap + padding * 2; let logical_height = 28; let physical_height = (logical_height as f32 * dpi_scale) as i32; let dib_width = total_content_width; let dib_height = physical_height; let screen_dc = GetDC(ptr::null_mut()); let mut bmi: BITMAPINFO = std::mem::zeroed(); bmi.bmiHeader.biSize = std::mem::size_of::() as u32; bmi.bmiHeader.biWidth = dib_width; bmi.bmiHeader.biHeight = -dib_height; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; let mut bits: *mut std::ffi::c_void = ptr::null_mut(); let hbitmap = CreateDIBSection(screen_dc, &bmi, DIB_RGB_COLORS, &mut bits, ptr::null_mut(), 0); ReleaseDC(ptr::null_mut(), screen_dc); if hbitmap.is_null() { DeleteObject(hfont as _); return; } let mem_dc = CreateCompatibleDC(ptr::null_mut()); let old_bmp = SelectObject(mem_dc, hbitmap as HGDIOBJ); let mut graphics: *mut GpGraphics = ptr::null_mut(); if GdipCreateFromHDC(mem_dc, &mut graphics) != 0 { SelectObject(mem_dc, old_bmp); DeleteObject(hbitmap as HGDIOBJ); DeleteDC(mem_dc); DeleteObject(hfont as _); return; } GdipSetSmoothingMode(graphics, SmoothingModeAntiAlias); let mut clear_brush: *mut GpSolidFill = ptr::null_mut(); GdipCreateSolidFill(0x00000000, &mut clear_brush); GdipFillRectangle(graphics, clear_brush as *mut GpBrush, 0.0, 0.0, dib_width as f32, dib_height as f32); GdipDeleteBrush(clear_brush as *mut GpBrush); let bg_argb: u32 = ((settings.opacity as u32) << 24) | 0x00111111; let mut bg_brush: *mut GpSolidFill = ptr::null_mut(); GdipCreateSolidFill(bg_argb, &mut bg_brush); GdipFillRectangle(graphics, bg_brush as *mut GpBrush, 0.0, 0.0, dib_width as f32, dib_height as f32); GdipDeleteBrush(bg_brush as *mut GpBrush); GdipDeleteGraphics(graphics); let old_font = SelectObject(mem_dc, hfont as _); SetBkMode(mem_dc, TRANSPARENT as i32); let gap = (10.0 * dpi_scale) as i32; let mut current_x: i32 = padding; let win_height_i32 = dib_height; for (i, item) in items.iter().enumerate() { if i > 0 { current_x += item_gap; } if !item.label.is_empty() { let wide_label: Vec = item.label.encode_utf16().chain(std::iter::once(0)).collect(); let mut label_rect = RECT { left: current_x, top: 0, right: current_x + item.label_width, bottom: win_height_i32, }; SetTextColor(mem_dc, font_color); DrawTextW( mem_dc, wide_label.as_ptr(), (wide_label.len() - 1) as i32, &mut label_rect, DT_RIGHT | DT_VCENTER | DT_SINGLELINE, ); } let value_x = if item.label.is_empty() { current_x } else { current_x + item.label_width + gap }; let wide_value: Vec = item.value.encode_utf16().chain(std::iter::once(0)).collect(); let mut value_rect = RECT { left: value_x, top: 0, right: value_x + item.value_width, bottom: win_height_i32, }; let mut color: u32 = font_color; if let Some(custom_color) = item.custom_color { color = custom_color; } else if !item.label.is_empty() && !item.value.contains("--") && (item.value.contains("°C") || item.value.contains('%') || item.value.bytes().all(|b| b.is_ascii_digit())) { let mut num_str = String::new(); for ch in item.value.chars() { if ch.is_ascii_digit() || ch == '.' { num_str.push(ch); } else if !num_str.is_empty() { break; } } if !num_str.is_empty() { if let Ok(nf) = num_str.parse::() { let nv = nf as i32; if nv < 50 { color = 0x0000FF00; } else if nv < 80 { color = 0x0000FFFF; } else { color = 0x000000FF; } } } } SetTextColor(mem_dc, color); DrawTextW( mem_dc, wide_value.as_ptr(), (wide_value.len() - 1) as i32, &mut value_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE, ); current_x += item.total_width; } SelectObject(mem_dc, old_font); if !bits.is_null() { let pixels = std::slice::from_raw_parts_mut( bits as *mut u32, (dib_width * dib_height) as usize, ); for pixel in pixels.iter_mut() { let alpha = (*pixel >> 24) & 0xFF; let rgb = *pixel & 0x00FFFFFF; if alpha == 0 && rgb != 0 { *pixel = 0xFF000000 | rgb; } } } let screen_width = GetSystemMetrics(SM_CXSCREEN); let default_x = (screen_width - dib_width) / 2; let use_x = settings.position_x.unwrap_or(default_x); let use_y = settings.position_y.unwrap_or(0); let ppt_dst = POINT { x: use_x, y: use_y }; let psize = SIZE { cx: dib_width, cy: dib_height }; let ppt_src = POINT { x: 0, y: 0 }; let blend = BLENDFUNCTION { BlendOp: AC_SRC_OVER as u8, BlendFlags: 0, SourceConstantAlpha: 255, AlphaFormat: AC_SRC_ALPHA as u8, }; UpdateLayeredWindow( hwnd, ptr::null_mut(), &ppt_dst, &psize, mem_dc, &ppt_src, 0, &blend, ULW_ALPHA, ); SelectObject(mem_dc, old_bmp); DeleteObject(hbitmap as HGDIOBJ); DeleteDC(mem_dc); DeleteObject(hfont as _); } pub unsafe fn draw_overlay_content_dynamic_island( hwnd: HWND, settings: &super::OverlaySettings, data: &super::OverlayHardwareData, ) { let dpi_scale = { let dc = GetDC(hwnd); let dpi = if dc.is_null() { 96 } else { GetDeviceCaps(dc, 88) }; if !dc.is_null() { ReleaseDC(hwnd, dc); } dpi as f32 / 96.0 }; let hfont = create_compatible_font(dpi_scale, &settings.font); if hfont.is_null() { return; } let font_color = parse_hex_color(&settings.font_color); // Build a temp DC just for measurement let temp_dc = GetDC(ptr::null_mut()); let mut items = build_display_items(settings, data); let padding = (16.0 * dpi_scale) as i32; let item_gap = (16.0 * dpi_scale) as i32; let content_width = measure_and_layout_items(temp_dc, hfont, &mut items, dpi_scale); ReleaseDC(ptr::null_mut(), temp_dc); let sep_count = if items.len() > 1 { items.len() as i32 - 1 } else { 0 }; let total_content_width = content_width + sep_count * item_gap + padding * 2; let logical_height = 36; let physical_height = (logical_height as f32 * dpi_scale) as i32; let dib_width = total_content_width; let dib_height = physical_height; // --- Create 32-bit ARGB DIB section (like crosshair) --- let screen_dc = GetDC(ptr::null_mut()); let mut bmi: BITMAPINFO = std::mem::zeroed(); bmi.bmiHeader.biSize = std::mem::size_of::() as u32; bmi.bmiHeader.biWidth = dib_width; bmi.bmiHeader.biHeight = -dib_height; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; let mut bits: *mut std::ffi::c_void = ptr::null_mut(); let hbitmap = CreateDIBSection(screen_dc, &bmi, DIB_RGB_COLORS, &mut bits, ptr::null_mut(), 0); ReleaseDC(ptr::null_mut(), screen_dc); if hbitmap.is_null() { DeleteObject(hfont as _); return; } let mem_dc = CreateCompatibleDC(ptr::null_mut()); let old_bmp = SelectObject(mem_dc, hbitmap as HGDIOBJ); // --- GDI+ anti-aliased rounded rect background --- let mut graphics: *mut GpGraphics = ptr::null_mut(); if GdipCreateFromHDC(mem_dc, &mut graphics) != 0 { SelectObject(mem_dc, old_bmp); DeleteObject(hbitmap as HGDIOBJ); DeleteDC(mem_dc); DeleteObject(hfont as _); return; } GdipSetSmoothingMode(graphics, SmoothingModeAntiAlias); // Clear to fully transparent let mut clear_brush: *mut GpSolidFill = ptr::null_mut(); GdipCreateSolidFill(0x00000000, &mut clear_brush); GdipFillRectangle(graphics, clear_brush as *mut GpBrush, 0.0, 0.0, dib_width as f32, dib_height as f32); GdipDeleteBrush(clear_brush as *mut GpBrush); // Draw rounded rect with GDI+ (proper per-pixel alpha anti-aliasing) let bg_argb: u32 = ((settings.opacity as u32) << 24) | 0x00111111; let corner_r = dib_height as f32 * 0.5; let mut bg_brush: *mut GpSolidFill = ptr::null_mut(); GdipCreateSolidFill(bg_argb, &mut bg_brush); let mut path: *mut GpPath = ptr::null_mut(); GdipCreatePath(FillModeAlternate, &mut path); if !path.is_null() { let w = dib_width as f32; let h = dib_height as f32; let r = corner_r; GdipAddPathArc(path, 0.0, 0.0, r * 2.0, r * 2.0, 180.0, 90.0); GdipAddPathLine(path, r, 0.0, w - r, 0.0); GdipAddPathArc(path, w - r * 2.0, 0.0, r * 2.0, r * 2.0, 270.0, 90.0); GdipAddPathLine(path, w, r, w, h - r); GdipAddPathArc(path, w - r * 2.0, h - r * 2.0, r * 2.0, r * 2.0, 0.0, 90.0); GdipAddPathLine(path, w - r, h, r, h); GdipAddPathArc(path, 0.0, h - r * 2.0, r * 2.0, r * 2.0, 90.0, 90.0); GdipAddPathLine(path, 0.0, h - r, 0.0, r); GdipClosePathFigure(path); GdipFillPath(graphics, bg_brush as *mut GpBrush, path); GdipDeletePath(path); } GdipDeleteBrush(bg_brush as *mut GpBrush); GdipDeleteGraphics(graphics); // --- Draw text using GDI --- let old_font = SelectObject(mem_dc, hfont as _); SetBkMode(mem_dc, TRANSPARENT as i32); let gap = (10.0 * dpi_scale) as i32; let mut current_x: i32 = padding; let win_height_i32 = dib_height; for (i, item) in items.iter().enumerate() { if i > 0 { current_x += item_gap; } if !item.label.is_empty() { let wide_label: Vec = item.label.encode_utf16().chain(std::iter::once(0)).collect(); let mut label_rect = RECT { left: current_x, top: 0, right: current_x + item.label_width, bottom: win_height_i32, }; SetTextColor(mem_dc, font_color); DrawTextW( mem_dc, wide_label.as_ptr(), (wide_label.len() - 1) as i32, &mut label_rect, DT_RIGHT | DT_VCENTER | DT_SINGLELINE, ); } let value_x = if item.label.is_empty() { current_x } else { current_x + item.label_width + gap }; let wide_value: Vec = item.value.encode_utf16().chain(std::iter::once(0)).collect(); let mut value_rect = RECT { left: value_x, top: 0, right: value_x + item.value_width, bottom: win_height_i32, }; let mut color: u32 = font_color; if let Some(custom_color) = item.custom_color { color = custom_color; } else if !item.label.is_empty() && !item.value.contains("--") && (item.value.contains("°C") || item.value.contains('%') || item.value.bytes().all(|b| b.is_ascii_digit())) { let mut num_str = String::new(); for ch in item.value.chars() { if ch.is_ascii_digit() || ch == '.' { num_str.push(ch); } else if !num_str.is_empty() { break; } } if !num_str.is_empty() { if let Ok(nf) = num_str.parse::() { let nv = nf as i32; if nv < 50 { color = 0x0000FF00; } else if nv < 80 { color = 0x0000FFFF; } else { color = 0x000000FF; } } } } SetTextColor(mem_dc, color); DrawTextW( mem_dc, wide_value.as_ptr(), (wide_value.len() - 1) as i32, &mut value_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE, ); current_x += item.total_width; } SelectObject(mem_dc, old_font); // Fix alpha for text pixels: GDI sets RGB but alpha stays 0 if !bits.is_null() { let pixels = std::slice::from_raw_parts_mut( bits as *mut u32, (dib_width * dib_height) as usize, ); for pixel in pixels.iter_mut() { let alpha = (*pixel >> 24) & 0xFF; let rgb = *pixel & 0x00FFFFFF; if alpha == 0 && rgb != 0 { *pixel = 0xFF000000 | rgb; } } } // --- Position and composite via UpdateLayeredWindow --- let screen_width = GetSystemMetrics(SM_CXSCREEN); let default_x = (screen_width - dib_width) / 2; let use_x = settings.position_x.unwrap_or(default_x); let use_y = settings.position_y.unwrap_or(4); let ppt_dst = POINT { x: use_x, y: use_y }; let psize = SIZE { cx: dib_width, cy: dib_height }; let ppt_src = POINT { x: 0, y: 0 }; let blend = BLENDFUNCTION { BlendOp: AC_SRC_OVER as u8, BlendFlags: 0, SourceConstantAlpha: 255, AlphaFormat: AC_SRC_ALPHA as u8, }; UpdateLayeredWindow( hwnd, ptr::null_mut(), &ppt_dst, &psize, mem_dc, &ppt_src, 0, &blend, ULW_ALPHA, ); SelectObject(mem_dc, old_bmp); DeleteObject(hbitmap as HGDIOBJ); DeleteDC(mem_dc); DeleteObject(hfont as _); } pub unsafe extern "system" fn window_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { match msg { WM_PAINT => { let mut ps = PAINTSTRUCT { hdc: ptr::null_mut(), fErase: 0, rcPaint: RECT { left: 0, top: 0, right: 0, bottom: 0, }, fRestore: 0, fIncUpdate: 0, rgbReserved: [0u8; 32], }; BeginPaint(hwnd, &mut ps); EndPaint(hwnd, &ps); 0 } WM_TIMER => { // 定时器 0.5s 刷新 SetTimer(hwnd, 1, 500, None); let data = super::collect_hardware_data(); *super::CURRENT_HARDWARE_DATA.lock().unwrap() = Some(data.clone()); let settings = super::get_or_init_settings(); if settings.style == "dynamic_island" { draw_overlay_content_dynamic_island(hwnd, &settings, &data); } else { draw_overlay_content(hwnd, &settings, &data); } 0 } WM_NCHITTEST => { // 拖动模式下返回 HTCAPTION 允许拖动 if super::DRAG_MODE.load(std::sync::atomic::Ordering::SeqCst) { HTCAPTION as LRESULT } else { DefWindowProcW(hwnd, msg, wparam, lparam) } } WM_EXITSIZEMOVE => { // 拖动结束后只保存位置,不退出拖动模式 // 退出由前端按钮控制,避免样式切换导致位置重置 if super::DRAG_MODE.load(std::sync::atomic::Ordering::SeqCst) { // 获取当前窗口位置 let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 }; GetWindowRect(hwnd, &mut rect); // 保存位置到设置 { let mut settings_lock = super::CURRENT_SETTINGS.lock().unwrap(); if let Some(ref mut settings) = *settings_lock { settings.position_x = Some(rect.left); settings.position_y = Some(rect.top); } } // 标记位置已变更 super::POSITION_CHANGED.store(true, std::sync::atomic::Ordering::SeqCst); } 0 } WM_DESTROY => { PostQuitMessage(0); 0 } _ => DefWindowProcW(hwnd, msg, wparam, lparam), } } } // 设置拖动模式 pub fn set_drag_mode(enabled: bool) { DRAG_MODE.store(enabled, Ordering::SeqCst); #[cfg(target_os = "windows")] unsafe { let hwnd = OVERLAY_HANDLE.load(Ordering::SeqCst); if !hwnd.is_null() { use windows_sys::Win32::UI::WindowsAndMessaging::*; // 获取当前窗口样式 let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; if enabled { // 进入拖动模式:移除 WS_EX_TRANSPARENT let new_style = ex_style & !WS_EX_TRANSPARENT; SetWindowLongW(hwnd, GWL_EXSTYLE, new_style as i32); } else { // 退出拖动模式:恢复 WS_EX_TRANSPARENT let new_style = ex_style | WS_EX_TRANSPARENT; SetWindowLongW(hwnd, GWL_EXSTYLE, new_style as i32); } // 刷新窗口样式 SetWindowPos( hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED, ); } } } #[cfg(target_os = "windows")] pub fn start_overlay(settings: OverlaySettings) -> Result { use windows_sys::Win32::UI::WindowsAndMessaging::*; if OVERLAY_ACTIVE.load(Ordering::SeqCst) { return Ok(OverlayResult { success: true, message: "悬浮框已处于启用状态".to_string(), }); } OVERLAY_ACTIVE.store(true, Ordering::SeqCst); { let mut settings_lock = CURRENT_SETTINGS.lock().unwrap(); *settings_lock = Some(settings.clone()); } thread::spawn(move || { use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_MULTITHREADED}; crate::game_ping::start_ping_thread(); crate::game_fps::start_fps_monitor(); let com_initialized = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED).is_ok() }; if !com_initialized { log::warn!("悬浮框线程初始化 COM 失败,网易云歌词功能可能不可用"); } unsafe { match win32::create_overlay_window(&settings) { std::result::Result::Ok(hwnd) => { OVERLAY_HANDLE.store(hwnd, Ordering::SeqCst); crate::game_fps::set_overlay_hwnd(hwnd as u64); if settings.style == "dynamic_island" { win32::draw_overlay_content_dynamic_island(hwnd, &settings, &CURRENT_HARDWARE_DATA.lock().unwrap().clone().unwrap_or_default()); } else { win32::draw_overlay_content(hwnd, &settings, &CURRENT_HARDWARE_DATA.lock().unwrap().clone().unwrap_or_default()); } SetTimer(hwnd, 1, 500, None); win32::install_topmost_guard(); let mut msg: MSG = std::mem::zeroed(); while OVERLAY_ACTIVE.load(Ordering::SeqCst) { while PeekMessageW(&mut msg, std::ptr::null_mut(), 0, 0, PM_REMOVE) != 0 { if msg.message == WM_QUIT { break; } TranslateMessage(&msg); DispatchMessageW(&msg); } if !OVERLAY_ACTIVE.load(Ordering::SeqCst) { break; } thread::sleep(Duration::from_millis(50)); } win32::uninstall_topmost_guard(); win32::destroy_overlay_window(hwnd); crate::game_fps::clear_overlay_hwnd(); OVERLAY_HANDLE.store(std::ptr::null_mut(), Ordering::SeqCst); } std::result::Result::Err(e) => { log::error!("创建悬浮框窗口失败: {}", e); OVERLAY_ACTIVE.store(false, Ordering::SeqCst); } } } if com_initialized { unsafe { CoUninitialize(); } } }); Ok(OverlayResult { success: true, message: "悬浮框已启动".to_string(), }) } #[cfg(target_os = "windows")] pub fn stop_overlay() -> Result { use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW; use windows_sys::Win32::UI::WindowsAndMessaging::WM_CLOSE; if !OVERLAY_ACTIVE.load(Ordering::SeqCst) { return Ok(OverlayResult { success: true, message: "悬浮框已处于关闭状态".to_string(), }); } OVERLAY_ACTIVE.store(false, Ordering::SeqCst); crate::game_ping::stop_ping_thread(); crate::game_fps::stop_fps_monitor(); unsafe { let hwnd = OVERLAY_HANDLE.load(Ordering::SeqCst); if !hwnd.is_null() { PostMessageW(hwnd, WM_CLOSE, 0, 0); } } Ok(OverlayResult { success: true, message: "悬浮框已关闭".to_string(), }) } #[cfg(not(target_os = "windows"))] pub fn start_overlay(_settings: OverlaySettings) -> Result { Err("此功能仅支持 Windows 系统".to_string()) } #[cfg(not(target_os = "windows"))] pub fn stop_overlay() -> Result { Err("此功能仅支持 Windows 系统".to_string()) } /// Check if Win32 overlay is active. pub fn is_overlay_active() -> bool { OVERLAY_ACTIVE.load(Ordering::SeqCst) } /// Toggle overlay on/off. Used by global hotkey. pub fn toggle_overlay(app_handle: &tauri::AppHandle) -> Result { // 如果当前样式是竖排面板,使用 Tauri 窗口方案 let settings = get_or_init_settings(); if settings.style == "vertical_panel" { return crate::vertical_overlay::toggle_vertical_overlay(app_handle); } // 如果竖排悬浮框正在运行,先停止它 if crate::vertical_overlay::is_vertical_overlay_active() { let handle = app_handle.clone(); let _ = tauri::async_runtime::block_on(async { crate::vertical_overlay::stop_vertical_overlay(handle).await }); } let result = if OVERLAY_ACTIVE.load(Ordering::SeqCst) { stop_overlay() } else { let settings = get_or_init_settings(); start_overlay(settings) }; if result.is_ok() { let _ = app_handle.emit("overlay-status-changed", ()); } result } #[tauri::command] pub async fn start_overlay_panel( app_handle: tauri::AppHandle, settings: Option, ) -> Result { let settings = settings.unwrap_or_else(get_or_init_settings); if settings.style == "vertical_panel" { return crate::vertical_overlay::start_vertical_overlay(app_handle, Some(settings)).await; } // 如果竖排悬浮框正在运行,先停止它 if crate::vertical_overlay::is_vertical_overlay_active() { let handle = app_handle.clone(); let _ = crate::vertical_overlay::stop_vertical_overlay(handle).await; std::thread::sleep(Duration::from_millis(200)); } start_overlay(settings) } #[tauri::command] pub async fn stop_overlay_panel(app_handle: tauri::AppHandle) -> Result { // 停止 Win32 overlay let win32_result = if OVERLAY_ACTIVE.load(Ordering::SeqCst) { stop_overlay() } else { Ok(OverlayResult { success: true, message: "Win32 悬浮框未运行".to_string(), }) }; // 停止竖排悬浮框 let vertical_result = crate::vertical_overlay::stop_vertical_overlay(app_handle).await; // 任一成功即视为成功 if win32_result.is_ok() || vertical_result.is_ok() { Ok(OverlayResult { success: true, message: "悬浮框已关闭".to_string(), }) } else { Err("悬浮框关闭失败".to_string()) } } #[tauri::command] pub async fn toggle_overlay_panel(app_handle: tauri::AppHandle) -> Result { toggle_overlay(&app_handle) } #[tauri::command] pub async fn get_overlay_panel_status() -> Result { Ok(OVERLAY_ACTIVE.load(Ordering::SeqCst) || crate::vertical_overlay::is_vertical_overlay_active()) } #[tauri::command] pub async fn get_overlay_hardware_data() -> Result { let data = CURRENT_HARDWARE_DATA .lock() .unwrap() .clone() .unwrap_or_default(); Ok(data) } #[tauri::command] pub async fn update_overlay_settings(app_handle: tauri::AppHandle, settings: OverlaySettings) -> Result { let (old_style, old_font) = { let lock = CURRENT_SETTINGS.lock().unwrap(); let s = lock.as_ref(); (s.map(|s| s.style.clone()), s.map(|s| s.font.clone())) }; let new_style = settings.style.clone(); let new_font = settings.font.clone(); let _old_was_vertical = old_style.as_deref() == Some("vertical_panel"); let new_is_vertical = new_style == "vertical_panel"; // 保存新设置 { let mut settings_lock = CURRENT_SETTINGS.lock().unwrap(); *settings_lock = Some(settings.clone()); } // 检查是否有 overlay 处于活跃状态 let win32_active = OVERLAY_ACTIVE.load(Ordering::SeqCst); let vertical_active = crate::vertical_overlay::is_vertical_overlay_active(); let any_active = win32_active || vertical_active; // 如果有任何悬浮框处于活跃状态 if any_active { let style_changed = old_style.as_deref() != Some(&new_style); let font_changed = old_font.as_deref() != Some(&new_font); if style_changed || font_changed { // 停止当前的 overlay if win32_active { stop_overlay()?; std::thread::sleep(Duration::from_millis(200)); } if vertical_active { let _ = crate::vertical_overlay::stop_vertical_overlay(app_handle.clone()).await; std::thread::sleep(Duration::from_millis(200)); } // 如果有活跃的 overlay,启动新样式的 overlay if any_active { if new_is_vertical { let _ = crate::vertical_overlay::start_vertical_overlay(app_handle, Some(settings)).await; } else { start_overlay(settings)?; let _ = app_handle.emit("overlay-status-changed", ()); } } } else if new_is_vertical { // 竖排面板且样式未变,仅推送设置更新 let _ = app_handle.emit("vertical-overlay-settings", &settings); } else { // Win32 overlay 且样式未变,直接重绘 #[cfg(target_os = "windows")] unsafe { let hwnd = OVERLAY_HANDLE.load(Ordering::SeqCst); if !hwnd.is_null() { let data = CURRENT_HARDWARE_DATA.lock().unwrap().clone().unwrap_or_default(); let current_settings = CURRENT_SETTINGS.lock().unwrap().clone().unwrap_or_default(); if new_style == "dynamic_island" { win32::draw_overlay_content_dynamic_island(hwnd, ¤t_settings, &data); } else { win32::draw_overlay_content(hwnd, ¤t_settings, &data); } } } } } Ok(OverlayResult { success: true, message: "设置已更新".to_string(), }) } #[tauri::command] pub async fn set_overlay_drag_mode(enabled: bool) -> Result { if !OVERLAY_ACTIVE.load(Ordering::SeqCst) { return Err("悬浮框未启用".to_string()); } #[cfg(target_os = "windows")] unsafe { use windows_sys::Win32::UI::WindowsAndMessaging::*; use windows_sys::Win32::Foundation::RECT; let hwnd = OVERLAY_HANDLE.load(Ordering::SeqCst); if hwnd.is_null() { return Err("悬浮框窗口不存在".to_string()); } if !enabled { // 退出拖动模式时,先保存当前位置 let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 }; GetWindowRect(hwnd, &mut rect); { let mut settings_lock = CURRENT_SETTINGS.lock().unwrap(); if let Some(ref mut settings) = *settings_lock { settings.position_x = Some(rect.left); settings.position_y = Some(rect.top); } } } // 切换拖动模式 set_drag_mode(enabled); if !enabled { // 退出拖动模式后,恢复窗口到保存的位置 let (saved_x, saved_y) = { let settings_lock = CURRENT_SETTINGS.lock().unwrap(); if let Some(ref settings) = *settings_lock { (settings.position_x, settings.position_y) } else { (None, None) } }; // 获取当前位置 let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 }; GetWindowRect(hwnd, &mut rect); // 如果位置发生变化,恢复到保存的位置 if let (Some(sx), Some(sy)) = (saved_x, saved_y) { if rect.left != sx || rect.top != sy { SetWindowPos( hwnd, HWND_TOPMOST, sx, sy, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE, ); } } POSITION_CHANGED.store(false, Ordering::SeqCst); } } let message = if enabled { "已进入拖动模式".to_string() } else { "已退出拖动模式".to_string() }; Ok(OverlayResult { success: true, message, }) } #[tauri::command] pub async fn get_overlay_current_settings() -> Result { let mut settings = CURRENT_SETTINGS.lock().unwrap().clone().unwrap_or_default(); // 合并默认项:新增的显示项自动追加到已有设置中 let defaults = default_display_items(); let mut merged = false; for default_item in &defaults { if !settings.display_items.iter().any(|i| i.id == default_item.id) { settings.display_items.push(default_item.clone()); merged = true; } } drop(defaults); // 释放借用 if merged { let mut lock = CURRENT_SETTINGS.lock().unwrap(); *lock = Some(settings.clone()); } Ok(settings) } #[tauri::command] pub async fn check_drag_mode_status() -> Result { // 返回当前拖动模式状态 Ok(DRAG_MODE.load(Ordering::SeqCst)) } #[tauri::command] pub async fn reset_overlay_position() -> Result { if !OVERLAY_ACTIVE.load(Ordering::SeqCst) { return Err("悬浮框未启用".to_string()); } #[cfg(target_os = "windows")] unsafe { use windows_sys::Win32::UI::WindowsAndMessaging::*; let hwnd = OVERLAY_HANDLE.load(Ordering::SeqCst); if hwnd.is_null() { return Err("悬浮框窗口不存在".to_string()); } // 清除已保存的位置,恢复默认居中 { let mut settings_lock = CURRENT_SETTINGS.lock().unwrap(); if let Some(ref mut settings) = *settings_lock { settings.position_x = None; settings.position_y = None; } } // 获取当前窗口大小 let mut rect = std::mem::zeroed(); GetWindowRect(hwnd, &mut rect); let win_w = rect.right - rect.left; let win_h = rect.bottom - rect.top; // 计算居中位置 let screen_width = GetSystemMetrics(SM_CXSCREEN); let screen_height = GetSystemMetrics(SM_CYSCREEN); let new_x = (screen_width - win_w) / 2; let new_y = (screen_height - win_h) / 2; // 移动窗口到居中位置 SetWindowPos( hwnd, HWND_TOPMOST, new_x, new_y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE, ); } Ok(OverlayResult { success: true, message: "位置已重置为默认".to_string(), }) } pub fn stop_hardware_poller() { BACKGROUND_POLLER_ACTIVE.store(false, Ordering::SeqCst); } pub fn start_hardware_poller() { if BACKGROUND_POLLER_ACTIVE.load(Ordering::SeqCst) { return; } BACKGROUND_POLLER_ACTIVE.store(true, Ordering::SeqCst); thread::spawn(|| { while BACKGROUND_POLLER_ACTIVE.load(Ordering::SeqCst) { let data = collect_hardware_data(); *CURRENT_HARDWARE_DATA.lock().unwrap() = Some(data.clone()); // 推送到硬件报告记录器(timestamp / elapsed_sec 由 push_snapshot 内部填充) crate::hardware_report::push_snapshot(crate::hardware_report::HardwareSnapshot { timestamp: String::new(), elapsed_sec: 0, cpu_usage: data.cpu_usage.map(|v| v as f64), cpu_temp: data.cpu_temp, cpu_clock: data.cpu_clock.map(|v| v as f64), cpu_voltage: data.cpu_voltage, cpu_power: data.cpu_power, cpu_fan_speed: data.cpu_fan_speed.map(|v| v as f64), gpu_usage: data.gpu_usage.map(|v| v as f64), gpu_temp: data.gpu_temp, gpu_clock: data.gpu_clock.map(|v| v as f64), gpu_voltage: data.gpu_voltage, gpu_power: data.gpu_power.map(|v| v as f64), gpu_fan_speed: data.gpu_fan_speed.map(|v| v as f64), gpu_vram_used: data.gpu_vram_used.map(|v| v as f64), gpu_vram_total: data.gpu_vram_total.map(|v| v as f64), gpu_memory_clock: data.gpu_memory_clock.map(|v| v as f64), memory_usage: data.memory_usage, ssd_temp: data.ssd_temp, }); thread::sleep(Duration::from_millis(1000)); } }); } pub fn cleanup() { stop_hardware_poller(); if OVERLAY_ACTIVE.load(Ordering::SeqCst) { let _ = stop_overlay(); } crate::game_ping::cleanup(); crate::game_fps::cleanup(); #[cfg(target_os = "windows")] unsafe { win32::shutdown_gdiplus(); } } #[tauri::command] pub async fn run_pawnio_setup() -> Result { let exe_dir = std::env::current_exe() .map_err(|e| format!("获取程序路径失败: {}", e))?; let parent_dir = exe_dir.parent().ok_or("无法获取父目录")?; let candidates = [ parent_dir.join("PawnIO_setup.exe"), parent_dir.join("_up_").join("PawnIO_setup.exe"), parent_dir.join("resources").join("PawnIO_setup.exe"), ]; for path in &candidates { if path.exists() { match std::process::Command::new(path).spawn() { Ok(_) => return Ok("安装程序已启动".to_string()), Err(e) => return Err(format!("启动安装程序失败: {}", e)), } } } Err("未找到 PawnIO_setup.exe,请确保已将其放在程序目录下".to_string()) }