Files
YMhut-box-C-/参考/NexBox-main/src-tauri/src/overlay_panel.rs
T

2137 lines
82 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<std::ffi::c_void> = 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<DisplayItem>;
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<CustomOverlayItem>,
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<i32>,
#[serde(default)]
pub position_y: Option<i32>,
/// 三角洲密码选中的地图名称列表(空 = 显示全部)
#[serde(default)]
pub delta_password_maps: Vec<String>,
}
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<u32>,
cpu_usage: Option<u16>,
cpu_temp: Option<f64>,
cpu_clock: Option<u32>,
gpu_temp: Option<f64>,
gpu_usage: Option<u32>,
memory_usage: Option<f64>,
delta_password: Option<String>,
game_ping: Option<u32>,
cpu_fan_speed: Option<u32>,
gpu_fan_speed: Option<u32>,
gpu_power: Option<u32>,
gpu_clock: Option<u32>,
gpu_vram_used: Option<u32>,
gpu_vram_total: Option<u32>,
gpu_memory_clock: Option<u32>,
cpu_voltage: Option<f64>,
gpu_voltage: Option<f64>,
cpu_power: Option<f64>,
ssd_temp: Option<f64>,
}
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<Option<OverlaySettings>> = Mutex::new(None);
pub static CURRENT_HARDWARE_DATA: Mutex<Option<OverlayHardwareData>> = 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<f64> {
let values: Vec<f64> = 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::<f64>() / 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 风扇传感器出现在 SuperIONuvoton 等)或 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<Option<usize>> = Mutex::new(None);
static WIN_EVENT_HOOK: Mutex<Option<usize>> = 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<HWND, String> {
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<u16> = 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<u16> = 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<u32>,
}
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<DisplayItem> {
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::<BITMAPINFOHEADER>() 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<u16> = 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<u16> = 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::<f32>() {
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::<BITMAPINFOHEADER>() 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<u16> = 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<u16> = 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::<f32>() {
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<OverlayResult, String> {
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<OverlayResult, String> {
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<OverlayResult, String> {
Err("此功能仅支持 Windows 系统".to_string())
}
#[cfg(not(target_os = "windows"))]
pub fn stop_overlay() -> Result<OverlayResult, String> {
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<OverlayResult, String> {
// 如果当前样式是竖排面板,使用 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<OverlaySettings>,
) -> Result<OverlayResult, String> {
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<OverlayResult, String> {
// 停止 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<OverlayResult, String> {
toggle_overlay(&app_handle)
}
#[tauri::command]
pub async fn get_overlay_panel_status() -> Result<bool, String> {
Ok(OVERLAY_ACTIVE.load(Ordering::SeqCst) || crate::vertical_overlay::is_vertical_overlay_active())
}
#[tauri::command]
pub async fn get_overlay_hardware_data() -> Result<OverlayHardwareData, String> {
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<OverlayResult, String> {
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, &current_settings, &data);
} else {
win32::draw_overlay_content(hwnd, &current_settings, &data);
}
}
}
}
}
Ok(OverlayResult {
success: true,
message: "设置已更新".to_string(),
})
}
#[tauri::command]
pub async fn set_overlay_drag_mode(enabled: bool) -> Result<OverlayResult, String> {
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<OverlaySettings, String> {
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<bool, String> {
// 返回当前拖动模式状态
Ok(DRAG_MODE.load(Ordering::SeqCst))
}
#[tauri::command]
pub async fn reset_overlay_position() -> Result<OverlayResult, String> {
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<String, String> {
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())
}