Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions electron/main/window/taskbarLyric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import type {
} from "@splayer/taskbar-lyric";
import { taskbarLog } from "@main/utils/logger";

type TaskbarLyricNative = typeof import("@splayer/taskbar-lyric");
type TaskbarLyricNative = typeof import("@splayer/taskbar-lyric") & {
/** 直接移动窗口位置(保持宽高不变) */
moveWindow?: (hwndPtr: number, x: number, y: number) => boolean;
};

const REG_SUBKEY_EXPLORER_ADVANCED =
"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced";
Expand Down Expand Up @@ -142,9 +145,18 @@ const pickSpace = (layout: JsTaskbarLayout): PickedSpace | null => {
/** 首次 applyLayout 成功后才 show 窗口——避免初始 3000x200 大窗口闪现 */
let firstLayoutDone = false;

/** 空间不足时安静隐藏窗口,避免残留旧位置的歌词残影 */
const hideIfVisible = (win: BrowserWindow): void => {
if (win.isVisible()) win.hide();
/**
* 收起态保持窗口可见,只把有效区域裁到远端 1×1
*
* hide/show 会让 Chromium 重新合成首帧,展开瞬间歌词内容先出现在偏移位置、
* 约 100ms 后才滑回原位;窗口保持可见时合成面持续有效,展开只恢复形状即可
* 直接呈现正确内容。1×1 区域落在窗口远端,不遮挡任务栏底缘的唤醒带
* @param win - 歌词窗口
*/
const parkWindow = (win: BrowserWindow): void => {
if (!firstLayoutDone || !win.isVisible()) return;
const { width } = win.getBounds();
win.setShape([{ x: Math.max(0, width - 1), y: 0, width: 1, height: 1 }]);
};

/** Rust 布局回调:把物理像素空间转为 DIP,<阈值则隐藏,≥阈值则 setBounds 并 show */
Expand All @@ -154,12 +166,12 @@ const applyLayout = (layout: JsTaskbarLayout): void => {

const picked = pickSpace(layout);
if (!picked) {
hideIfVisible(win);
parkWindow(win);
return;
}
const { rect, anchor } = picked;
if (rect.width <= 0 || rect.height <= 0) {
hideIfVisible(win);
parkWindow(win);
return;
}

Expand All @@ -175,7 +187,7 @@ const applyLayout = (layout: JsTaskbarLayout): void => {
const availHeight = Math.round(rect.height / dpi);

if (availWidth < MIN_LYRIC_WIDTH_DIP) {
hideIfVisible(win);
parkWindow(win);
return;
}

Expand All @@ -191,7 +203,14 @@ const applyLayout = (layout: JsTaskbarLayout): void => {
height: availHeight,
anchor,
};
win.setBounds({ x: windowX, y: availY, width: windowWidth, height: availHeight });
const hwndPtr = Number(win.getNativeWindowHandle().readBigUInt64LE(0));
// Electron 的 setBounds 移动 owner 窗口不生效,交由 native 直接 SetWindowPos;
// 布局矩形与 SetWindowPos 同为物理像素,无需 DIP 换算
const moved =
!!nativeModule &&
typeof nativeModule.moveWindow === "function" &&
nativeModule.moveWindow(hwndPtr, rect.x, rect.y);
if (!moved) win.setBounds({ x: windowX, y: availY, width: windowWidth, height: availHeight });
applyContentShape();

if (!firstLayoutDone) {
Expand Down Expand Up @@ -336,6 +355,10 @@ export const createTaskbarLyricWindow = (): BrowserWindow | null => {
const hwndPtr = Number(hwndPtrBigInt);
taskbarLog.info(`嵌入窗口 hwnd=${hwndPtr}`);
svc.embedWindowByPtr(hwndPtr);
// 首帧先以 1×1 形状显示窗口:Chromium 对首次 show 的合成可能使用旧内容位置,
// 先让窗口在不可见形状下完成首帧合成,之后应用真实形状时内容位置才是准确的
win.setShape([{ x: INITIAL_WIDTH - 1, y: 0, width: 1, height: 1 }]);
win.showInactive();
svc.update(resolveLyricWidth());
startWatchers(mod);
taskbarCreatedWatcher = tryStart(
Expand Down
8 changes: 8 additions & 0 deletions native/taskbar-lyric/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,11 @@ export interface JsTaskbarLayout {
space: JsAvailableSpace
extra: JsExtraLayoutInfo
}

/**
* 直接移动窗口位置,保持宽高不变
*
* Electron 对嵌入任务栏的透明无边框窗口,setBounds 的 move 部分在 compositor surface
* 已建立后不会真正移动 HWND;动画跟随需要直接调 SetWindowPos
*/
export declare function moveWindow(hwndPtr: number, x: number, y: number): boolean
26 changes: 26 additions & 0 deletions native/taskbar-lyric/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,32 @@ pub(crate) fn take_valid_hwnd(hwnd_ptr: usize) -> Option<HWND> {
}
}

/// 直接移动窗口位置,保持宽高不变
///
/// Electron 对嵌入任务栏的透明无边框窗口,setBounds 的 move 部分在 compositor surface
/// 已建立后不会真正移动 HWND;动画跟随需要直接调 SetWindowPos
#[napi]
pub fn move_window(hwnd_ptr: f64, x: i32, y: i32) -> bool {
use windows::Win32::UI::WindowsAndMessaging::{
SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, SetWindowPos,
};
let Some(hwnd) = take_valid_hwnd(hwnd_ptr as usize) else {
return false;
};
unsafe {
SetWindowPos(
hwnd,
None,
x,
y,
0,
0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
)
.is_ok()
}
}

// --- NAPI Watcher 绑定 ---

#[napi(js_name = "UiaWatcher")]
Expand Down
24 changes: 20 additions & 4 deletions native/taskbar-lyric/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,15 @@ fn worker_loop(rx: &Receiver<TaskbarCommand>, tsfn: &LayoutTsfn) {
}

TaskbarCommand::Update { width } => {
let mut final_width = width;
let mut widths = vec![width];
let mut stop_signal = false;
let mut reinit_requested = false;

while let Ok(next_msg) = rx.try_recv() {
match next_msg {
TaskbarCommand::Update { width: w } => final_width = w,
TaskbarCommand::Update { width: w } => {
widths.push(w);
}
TaskbarCommand::Embed { hwnd_ptr } => {
if let Some(hwnd) = take_valid_hwnd(hwnd_ptr) {
last_hwnd = Some(hwnd_ptr);
Expand All @@ -154,13 +156,27 @@ fn worker_loop(rx: &Receiver<TaskbarCommand>, tsfn: &LayoutTsfn) {
break;
}

last_width = final_width;
last_width = *widths.last().unwrap_or(&last_width);

if reinit_requested {
do_reinit(&mut strategy, last_hwnd);
// 重建后历史帧对应的旧布局已无意义,只按最新宽度重放一次
let last = widths.pop().unwrap_or(last_width);
widths.clear();
widths.push(last);
}

if !run_update_with_retry(&mut strategy, final_width, tsfn, rx) {
// 自动隐藏的滑动动画由 LOCATIONCHANGE 每帧驱动,每个 Update 都对应一帧位置;
// 合并成最后一帧会让歌词在动画期间钉在原地、等动画结束才跳到终点
let mut stop = false;
for w in widths {
last_width = w;
if !run_update_with_retry(&mut strategy, w, tsfn, rx) {
stop = true;
break;
}
}
if stop {
break;
}
}
Expand Down
Loading