feat: add Minecraft launch transition animation (cover/logo phases)
This commit is contained in:
@ -92,6 +92,7 @@ features = [
|
||||
"Foundation",
|
||||
"UI_ViewManagement",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
|
||||
@ -253,29 +253,22 @@ impl LightweightMode {
|
||||
if payload.maximize_window {
|
||||
maximize_minecraft_window(payload.pid).await;
|
||||
}
|
||||
let settings = match theseus::settings::get().await {
|
||||
Ok(settings) => settings,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"Failed to read lightweight mode setting: {error}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if settings.enter_lightweight_mode_on_game_launch {
|
||||
let state = app.state::<LightweightMode>();
|
||||
if let Err(error) = state.enter(&app) {
|
||||
tracing::warn!(
|
||||
"Failed to enter lightweight mode: {error}"
|
||||
);
|
||||
}
|
||||
} else if settings.hide_on_process_start
|
||||
&& let Some(window) =
|
||||
app.get_webview_window(MAIN_WINDOW_LABEL)
|
||||
&& let Err(error) = window.minimize()
|
||||
{
|
||||
// 窗口过渡:把启动器全屏遮罩 → 缩放 → 淡出 → 最小化
|
||||
crate::mc_transition::run(
|
||||
&app,
|
||||
payload.pid,
|
||||
payload.maximize_window,
|
||||
)
|
||||
.await;
|
||||
// 过渡动画结束后,统一进入轻量模式(隐藏到托盘)。
|
||||
// 由过渡流程接管,不再各自判断用户的「轻量模式 /
|
||||
// 启动后隐藏」设置,避免与动画收尾冲突,也顺带修掉
|
||||
// 「重开时窗口全透明」的问题(轻量模式恢复时是重建
|
||||
// 全新窗口,不残留旧的 opacity)。
|
||||
let state = app.state::<LightweightMode>();
|
||||
if let Err(error) = state.enter(&app) {
|
||||
tracing::warn!(
|
||||
"Failed to minimize launcher after Minecraft started: {error}"
|
||||
"Failed to enter lightweight mode after transition: {error}"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -22,6 +22,7 @@ use theseus::prelude::*;
|
||||
mod api;
|
||||
mod error;
|
||||
mod lightweight_mode;
|
||||
mod mc_transition;
|
||||
mod mod_translation;
|
||||
mod portable;
|
||||
mod seed_map;
|
||||
@ -867,6 +868,10 @@ fn main() {
|
||||
lightweight_mode::lightweight_mode_frontend_ready,
|
||||
lightweight_mode::lightweight_mode_set_route,
|
||||
lightweight_mode::lightweight_mode_enter,
|
||||
mc_transition::mc_transition_fade_done,
|
||||
mc_transition::mc_transition_cover_ready,
|
||||
mc_transition::mc_transition_logo_in_done,
|
||||
mc_transition::mc_transition_logo_out_done,
|
||||
]);
|
||||
|
||||
tracing::info!("Initializing app...");
|
||||
|
||||
470
apps/app/src/mc_transition.rs
Normal file
470
apps/app/src/mc_transition.rs
Normal file
@ -0,0 +1,470 @@
|
||||
//! Minecraft 启动时的窗口过渡动画(仅 Windows 生效)。
|
||||
//!
|
||||
//! 流程(四阶段):
|
||||
//! 阶段A:启动器放大到全屏(1s 动画),全程置顶 + 聚焦;
|
||||
//! 阶段B:等待 MC 窗口出现(最多 60s),期间启动器盖住游戏;
|
||||
//! 阶段C:获取 MC 窗口位置/大小,1s 动画缩放到相同大小;
|
||||
//! 阶段D:大小一致后,0.5s 淡出(不与缩放同步);
|
||||
//! 阶段E:聚焦游戏窗口 → 启动器最小化。
|
||||
//!
|
||||
//! 过渡结束后返回,由调用方继续执行用户的轻量模式 / 隐藏设置。
|
||||
//!
|
||||
//! 注意:HWND 是裸指针,非 Send,不能跨 await 持有,因此一律用 `usize`
|
||||
//! 保存窗口句柄,只在同步的 win32 调用里临时转回 HWND。
|
||||
|
||||
#![cfg_attr(not(target_os = "windows"), allow(dead_code, unused_imports))]
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||
|
||||
/// 主窗口 label,与 lightweight_mode.rs 保持一致。
|
||||
pub const MAIN_WINDOW_LABEL: &str = "main";
|
||||
|
||||
/// 等待 MC 窗口出现的轮询次数与间隔(120 × 500ms = 60s)。
|
||||
const FIND_RETRIES: u32 = 120;
|
||||
const FIND_INTERVAL_MS: u64 = 500;
|
||||
/// 缩放动画总时长与帧数(1000ms / 50 帧 = 20ms 一帧)。
|
||||
const ANIM_DURATION_MS: u64 = 400;
|
||||
const ANIM_STEPS: u32 = 50;
|
||||
/// 等待前端完成 CSS 淡出的超时兜底(毫秒)。淡出 500ms,留足余量。
|
||||
const FADE_TIMEOUT_MS: u64 = 700;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static FADE_DONE: std::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<()>>,
|
||||
> = std::sync::Mutex::new(None);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static COVER_DONE: std::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<()>>,
|
||||
> = std::sync::Mutex::new(None);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static LOGO_IN_DONE: std::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<()>>,
|
||||
> = std::sync::Mutex::new(None);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static LOGO_OUT_DONE: std::sync::Mutex<
|
||||
Option<tokio::sync::oneshot::Sender<()>>,
|
||||
> = std::sync::Mutex::new(None);
|
||||
|
||||
/// 调试日志:直接追加到用户目录下固定文件,绕过 tracing 配置。
|
||||
/// TODO(上线前清理):连同所有 `dbg(...)` 调用一起删除。
|
||||
fn dbg(msg: &str) {
|
||||
use std::io::Write;
|
||||
let base =
|
||||
std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\".to_string());
|
||||
let path = format!("{base}\\mc_transition_debug.log");
|
||||
if let Ok(mut f) =
|
||||
std::fs::OpenOptions::new().create(true).append(true).open(path)
|
||||
{
|
||||
let _ = writeln!(f, "{msg}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 前端完成淡出动画后的回调命令。
|
||||
#[tauri::command]
|
||||
pub fn mc_transition_fade_done() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dbg("fade_done callback");
|
||||
if let Ok(mut guard) = FADE_DONE.lock()
|
||||
&& let Some(tx) = guard.take()
|
||||
{
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 前端覆盖层渐显完成后的回调命令。
|
||||
#[tauri::command]
|
||||
pub fn mc_transition_cover_ready() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dbg("cover_ready callback");
|
||||
if let Ok(mut guard) = COVER_DONE.lock()
|
||||
&& let Some(tx) = guard.take()
|
||||
{
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 前端 SVG logo 渐显完成后的回调命令。
|
||||
#[tauri::command]
|
||||
pub fn mc_transition_logo_in_done() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dbg("logo_in_done callback");
|
||||
if let Ok(mut guard) = LOGO_IN_DONE.lock()
|
||||
&& let Some(tx) = guard.take()
|
||||
{
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 前端 SVG logo 渐隐 + 背景变红完成后的回调命令。
|
||||
#[tauri::command]
|
||||
pub fn mc_transition_logo_out_done() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dbg("logo_out_done callback");
|
||||
if let Ok(mut guard) = LOGO_OUT_DONE.lock()
|
||||
&& let Some(tx) = guard.take()
|
||||
{
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 缓入缓出曲线,比纯 ease-out 更柔和自然。
|
||||
#[cfg(target_os = "windows")]
|
||||
fn ease_in_out(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
4.0 * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 分帧移动 + 缩放启动器窗口。`from`/`to` 为 (x, y, w, h)。
|
||||
/// 若起止相同则直接返回;结束时补一帧精确坐标,避免累积误差。
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn animate_resize(
|
||||
window: &WebviewWindow,
|
||||
from: (i32, i32, u32, u32),
|
||||
to: (i32, i32, u32, u32),
|
||||
duration_ms: u64,
|
||||
) {
|
||||
use tauri::{PhysicalPosition, PhysicalSize};
|
||||
|
||||
if from == to {
|
||||
return;
|
||||
}
|
||||
let step_ms = (duration_ms / ANIM_STEPS as u64).max(1);
|
||||
for step in 1..=ANIM_STEPS {
|
||||
let t = step as f64 / ANIM_STEPS as f64;
|
||||
let e = ease_in_out(t);
|
||||
let x = from.0 as f64 + (to.0 - from.0) as f64 * e;
|
||||
let y = from.1 as f64 + (to.1 - from.1) as f64 * e;
|
||||
let w = from.2 as f64 + (to.2 as i64 - from.2 as i64) as f64 * e;
|
||||
let h = from.3 as f64 + (to.3 as i64 - from.3 as i64) as f64 * e;
|
||||
let _ =
|
||||
window.set_position(PhysicalPosition::new(x as i32, y as i32));
|
||||
let _ = window.set_size(PhysicalSize::new(
|
||||
w.max(1.0) as u32,
|
||||
h.max(1.0) as u32,
|
||||
));
|
||||
tokio::time::sleep(std::time::Duration::from_millis(step_ms)).await;
|
||||
}
|
||||
// 补最后一帧精确值
|
||||
let _ = window.set_position(PhysicalPosition::new(to.0, to.1));
|
||||
let _ = window.set_size(PhysicalSize::new(to.2.max(1), to.3.max(1)));
|
||||
}
|
||||
pub async fn run(app: &AppHandle, pid: u32, _maximize: bool) {
|
||||
if pid == 0 {
|
||||
dbg("run() aborted: pid=0");
|
||||
return;
|
||||
}
|
||||
let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) else {
|
||||
dbg("run() aborted: main window not found");
|
||||
return;
|
||||
};
|
||||
dbg(&format!("run() invoked pid={pid}"));
|
||||
|
||||
// 记录启动器当前矩形作为起点
|
||||
let start = {
|
||||
let p = window.outer_position().map(|p| (p.x, p.y)).unwrap_or((0, 0));
|
||||
let s =
|
||||
window.outer_size().map(|s| (s.width, s.height)).unwrap_or((1280, 800));
|
||||
(p.0, p.1, s.0, s.1)
|
||||
};
|
||||
dbg(&format!("launcher start rect = {start:?}"));
|
||||
|
||||
// 全屏目标 = 启动器当前所在显示器的完整矩形
|
||||
let fullscreen = match window
|
||||
.current_monitor()
|
||||
.ok()
|
||||
.flatten()
|
||||
.or_else(|| window.primary_monitor().ok().flatten())
|
||||
{
|
||||
Some(m) => {
|
||||
(m.position().x, m.position().y, m.size().width, m.size().height)
|
||||
}
|
||||
None => {
|
||||
dbg("no monitor info, abort");
|
||||
return;
|
||||
}
|
||||
};
|
||||
dbg(&format!("fullscreen target = {fullscreen:?}"));
|
||||
|
||||
// 全程置顶 + 聚焦
|
||||
let _ = window.set_always_on_top(true);
|
||||
let _ = window.set_focus();
|
||||
|
||||
// ---------- 第①步:显示纯色遮罩(无图,主题色) ----------
|
||||
dbg("step 1: show plain cover");
|
||||
let (cover_tx, cover_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
if let Ok(mut guard) = COVER_DONE.lock() {
|
||||
*guard = Some(cover_tx);
|
||||
}
|
||||
let _ = app.emit("mc-transition-cover-show", ());
|
||||
let covered = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
cover_rx,
|
||||
)
|
||||
.await;
|
||||
dbg(&format!("step 1: cover ready ok={}", covered.is_ok()));
|
||||
|
||||
// ---------- 第②步:放大到全屏(1s) ----------
|
||||
dbg("step 2: expand to fullscreen");
|
||||
animate_resize(&window, start, fullscreen, ANIM_DURATION_MS).await;
|
||||
dbg("step 2 done");
|
||||
|
||||
// ---------- 第③步:全屏后,渐显 SVG logo ----------
|
||||
dbg("step 3: fade in logo");
|
||||
let (lin_tx, lin_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
if let Ok(mut guard) = LOGO_IN_DONE.lock() {
|
||||
*guard = Some(lin_tx);
|
||||
}
|
||||
let _ = app.emit("mc-transition-logo-in", ());
|
||||
let logo_in = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1500),
|
||||
lin_rx,
|
||||
)
|
||||
.await;
|
||||
dbg(&format!("step 3: logo in done ok={}", logo_in.is_ok()));
|
||||
|
||||
// ---------- 等待 MC 窗口出现(最多 60s) ----------
|
||||
dbg("waiting for MC window");
|
||||
let mut mc_raw: usize = 0;
|
||||
for i in 0..FIND_RETRIES {
|
||||
if let Some(raw) = win_impl::find_window(pid) {
|
||||
mc_raw = raw;
|
||||
dbg(&format!("MC window found at try#{i} hwnd={raw:#x}"));
|
||||
break;
|
||||
}
|
||||
if i % 4 == 0 {
|
||||
dbg(&format!(
|
||||
"try#{i} pid={pid} candidates: {:?}",
|
||||
win_impl::dump_pid_windows(pid)
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(FIND_INTERVAL_MS))
|
||||
.await;
|
||||
}
|
||||
if mc_raw == 0 {
|
||||
dbg("MC window NOT FOUND -> restore launcher");
|
||||
let _ = window.set_always_on_top(false);
|
||||
animate_resize(&window, fullscreen, start, 300).await;
|
||||
let _ = app.emit("mc-transition-reset-opacity", ());
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- 第④步:缩小前,背景变 Mojang 红 + 渐隐 logo ----------
|
||||
dbg("step 4: turn red + fade out logo");
|
||||
let (lout_tx, lout_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
if let Ok(mut guard) = LOGO_OUT_DONE.lock() {
|
||||
*guard = Some(lout_tx);
|
||||
}
|
||||
let _ = app.emit("mc-transition-logo-out", ());
|
||||
let logo_out = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1500),
|
||||
lout_rx,
|
||||
)
|
||||
.await;
|
||||
dbg(&format!("step 4: logo out done ok={}", logo_out.is_ok()));
|
||||
|
||||
// ---------- 第⑤步:缩小到 MC 窗口矩形(1s) ----------
|
||||
let mc_rect = win_impl::get_rect(mc_raw).unwrap_or((
|
||||
fullscreen.0,
|
||||
fullscreen.1,
|
||||
fullscreen.2 as i32,
|
||||
fullscreen.3 as i32,
|
||||
));
|
||||
let covers = win_impl::covers_monitor(mc_raw);
|
||||
dbg(&format!("step 5: MC rect={mc_rect:?} covers_monitor={covers}"));
|
||||
let target = (
|
||||
mc_rect.0,
|
||||
mc_rect.1,
|
||||
mc_rect.2.max(1) as u32,
|
||||
mc_rect.3.max(1) as u32,
|
||||
);
|
||||
let _ = window.set_always_on_top(true);
|
||||
let _ = window.set_focus();
|
||||
animate_resize(&window, fullscreen, target, ANIM_DURATION_MS).await;
|
||||
dbg("step 5 done");
|
||||
|
||||
// ---------- 第⑥步:等待 1s(游戏稳定,遮罩仍在) ----------
|
||||
dbg("step 6: settle 1s");
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
dbg("step 6 done");
|
||||
|
||||
// ---------- 第⑦步:淡出(0.5s,含遮罩一起) ----------
|
||||
dbg("step 7: fade out, waiting frontend");
|
||||
let (done_tx, done_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
if let Ok(mut guard) = FADE_DONE.lock() {
|
||||
*guard = Some(done_tx);
|
||||
}
|
||||
let _ = app.emit("mc-transition-fadeout", ());
|
||||
let faded = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(FADE_TIMEOUT_MS),
|
||||
done_rx,
|
||||
)
|
||||
.await;
|
||||
dbg(&format!("step 7: fade wait ok={}", faded.is_ok()));
|
||||
|
||||
// ---------- 第⑧步:聚焦游戏 + 最小化启动器 ----------
|
||||
dbg("step 8: focus game + minimize launcher");
|
||||
let _ = window.set_always_on_top(false);
|
||||
win_impl::focus(mc_raw);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
|
||||
let _ = window.minimize();
|
||||
let _ = app.emit("mc-transition-reset-opacity", ());
|
||||
dbg("step 8 done (launcher minimized)");
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub async fn run(_app: &AppHandle, _pid: u32, _maximize: bool) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win_impl {
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU32, AtomicUsize, Ordering},
|
||||
};
|
||||
use windows::Win32::Foundation::{HWND, LPARAM, RECT};
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
EnumWindows, GetWindowRect, GetWindowThreadProcessId, IsWindowVisible,
|
||||
SetForegroundWindow,
|
||||
};
|
||||
use windows::core::BOOL;
|
||||
|
||||
/// 目标进程 pid(枚举回调通过静态变量传递)。
|
||||
static TARGET_PID: AtomicU32 = AtomicU32::new(0);
|
||||
/// 命中的窗口句柄,以 usize 存储(HWND 非 Send/Sync)。
|
||||
static FOUND_HWND: AtomicUsize = AtomicUsize::new(0);
|
||||
/// 串行化枚举,避免并发查找互相覆盖静态变量。
|
||||
static ENUM_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// usize → HWND。调用方保证数值来自真实句柄。
|
||||
fn to_hwnd(raw: usize) -> HWND {
|
||||
HWND(raw as *mut core::ffi::c_void)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn find_cb(hwnd: HWND, _: LPARAM) -> BOOL {
|
||||
let mut pid = 0u32;
|
||||
unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) };
|
||||
if pid != TARGET_PID.load(Ordering::Relaxed)
|
||||
|| !unsafe { IsWindowVisible(hwnd).as_bool() }
|
||||
{
|
||||
return BOOL(1);
|
||||
}
|
||||
let mut rect = RECT::default();
|
||||
if unsafe { GetWindowRect(hwnd, &mut rect) }.is_ok() {
|
||||
let w = rect.right - rect.left;
|
||||
let h = rect.bottom - rect.top;
|
||||
// 过滤掉工具窗口 / 0 尺寸窗口,只认真正的游戏主窗口
|
||||
if w > 50 && h > 50 {
|
||||
FOUND_HWND.store(hwnd.0 as usize, Ordering::Relaxed);
|
||||
return BOOL(0); // 找到即停止枚举
|
||||
}
|
||||
}
|
||||
BOOL(1)
|
||||
}
|
||||
|
||||
/// 诊断用:列出属于 `pid` 的所有顶层窗口(含不可见/小窗口)。
|
||||
/// 返回 (hwnd, 宽, 高, 是否可见)。
|
||||
pub fn dump_pid_windows(pid: u32) -> Vec<(usize, i32, i32, bool)> {
|
||||
use windows::core::BOOL as B;
|
||||
|
||||
static DUMP_PID: AtomicU32 = AtomicU32::new(0);
|
||||
static DUMP: Mutex<Vec<(usize, i32, i32, bool)>> =
|
||||
Mutex::new(Vec::new());
|
||||
|
||||
unsafe extern "system" fn cb(hwnd: HWND, _: LPARAM) -> B {
|
||||
let mut p = 0u32;
|
||||
unsafe { GetWindowThreadProcessId(hwnd, Some(&mut p)) };
|
||||
if p != DUMP_PID.load(Ordering::Relaxed) {
|
||||
return B(1);
|
||||
}
|
||||
let mut r = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(hwnd, &mut r) };
|
||||
let vis = unsafe { IsWindowVisible(hwnd).as_bool() };
|
||||
if let Ok(mut v) = DUMP.lock() {
|
||||
v.push((hwnd.0 as usize, r.right - r.left, r.bottom - r.top, vis));
|
||||
}
|
||||
B(1)
|
||||
}
|
||||
|
||||
let _g = ENUM_LOCK.lock();
|
||||
let _g = match _g {
|
||||
Ok(g) => g,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
DUMP_PID.store(pid, Ordering::Relaxed);
|
||||
if let Ok(mut v) = DUMP.lock() {
|
||||
v.clear();
|
||||
}
|
||||
unsafe {
|
||||
let _ = EnumWindows(Some(cb), LPARAM(0));
|
||||
}
|
||||
DUMP.lock().map(|v| v.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 查找属于 `pid` 的可见顶层窗口,返回句柄原始值(0 表示未找到)。
|
||||
pub fn find_window(pid: u32) -> Option<usize> {
|
||||
let _guard = ENUM_LOCK.lock().ok()?;
|
||||
TARGET_PID.store(pid, Ordering::Relaxed);
|
||||
FOUND_HWND.store(0, Ordering::Relaxed);
|
||||
unsafe {
|
||||
let _ = EnumWindows(Some(find_cb), LPARAM(0));
|
||||
}
|
||||
let raw = FOUND_HWND.load(Ordering::Relaxed);
|
||||
(raw != 0).then_some(raw)
|
||||
}
|
||||
|
||||
/// 取窗口矩形,返回 (x, y, width, height)。
|
||||
pub fn get_rect(raw: usize) -> Option<(i32, i32, i32, i32)> {
|
||||
let mut r = RECT::default();
|
||||
unsafe { GetWindowRect(to_hwnd(raw), &mut r).ok()? };
|
||||
Some((r.left, r.top, r.right - r.left, r.bottom - r.top))
|
||||
}
|
||||
|
||||
/// 取窗口所在显示器的完整矩形,返回 (x, y, width, height)。
|
||||
pub fn monitor_rect(raw: usize) -> Option<(i32, i32, i32, i32)> {
|
||||
let mon =
|
||||
unsafe { MonitorFromWindow(to_hwnd(raw), MONITOR_DEFAULTTONEAREST) };
|
||||
let mut mi = MONITORINFO {
|
||||
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
|
||||
rcMonitor: RECT::default(),
|
||||
rcWork: RECT::default(),
|
||||
dwFlags: 0,
|
||||
};
|
||||
if !unsafe { GetMonitorInfoW(mon, &mut mi) }.as_bool() {
|
||||
return None;
|
||||
}
|
||||
let r = mi.rcMonitor;
|
||||
Some((r.left, r.top, r.right - r.left, r.bottom - r.top))
|
||||
}
|
||||
|
||||
/// 判断窗口是否铺满其所在显示器(全屏 / 无边框全屏 / 最大化)。
|
||||
pub fn covers_monitor(raw: usize) -> bool {
|
||||
let (Some((_, _, w, h)), Some((_, _, mw, mh))) =
|
||||
(get_rect(raw), monitor_rect(raw))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
w >= mw && h >= mh
|
||||
}
|
||||
|
||||
/// 把前台焦点交给游戏窗口。
|
||||
pub fn focus(raw: usize) {
|
||||
unsafe {
|
||||
let _ = SetForegroundWindow(to_hwnd(raw));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user