feat(rust): native Win32 window opacity via SetLayeredWindowAttributes
Tauri 2.11 doesn't expose set_opacity on its WebviewWindow (open issue tauri-apps/tauri#3279). Drop down to the Win32 API directly: ensure WS_EX_LAYERED is set on the overlay HWND, then SetLayeredWindowAttributes with an 0..255 alpha. This is true OS-level window translucency — the same call Tauri will eventually wrap upstream — not a CSS shim. windows crate version is pinned to 0.61 to match Tauri 2.11.x's transitive dependency, so WebviewWindow::hwnd() returns a compatible HWND with no diamond-dependency conflict. Replaces the earlier eval-based opacity workaround in apply_state and set_window_opacity. Both call sites now go through apply_window_opacity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -320,6 +320,7 @@ dependencies = [
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -30,3 +30,11 @@ tokio = { version = "1", features = ["time", "macros"] }
|
||||
tauri-plugin-window-state = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
# Match Tauri 2.11.x's transitive windows version so `WebviewWindow::hwnd()`
|
||||
# returns a compatible HWND (no diamond-dependency conflict).
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Foundation::HWND;
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW, GWL_EXSTYLE, LWA_ALPHA,
|
||||
WS_EX_LAYERED,
|
||||
};
|
||||
|
||||
pub const OVERLAY_LABEL: &str = "overlay";
|
||||
pub const EVT_CLICK_THROUGH_TOGGLED: &str = "click-through-toggled";
|
||||
@@ -13,6 +21,41 @@ pub struct ClickThroughToggledPayload {
|
||||
pub click_through: bool,
|
||||
}
|
||||
|
||||
/// Native window opacity via SetLayeredWindowAttributes.
|
||||
///
|
||||
/// Tauri 2.11 doesn't expose set_opacity on its WebviewWindow (open issue
|
||||
/// tauri-apps/tauri#3279). We drop down to Win32 directly: ensure the window
|
||||
/// has the WS_EX_LAYERED extended style, then set its alpha. This is the
|
||||
/// same call Tauri will eventually wrap when upstream support lands —
|
||||
/// migrating later is a one-liner.
|
||||
///
|
||||
/// `opacity` is clamped to 0.0..=1.0 and translated to a 0..=255 alpha byte.
|
||||
fn apply_window_opacity<R: Runtime>(window: &WebviewWindow<R>, opacity: f32) -> Result<(), String> {
|
||||
let clamped = opacity.clamp(0.0, 1.0);
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let hwnd: HWND = window.hwnd().map_err(|e| e.to_string())?;
|
||||
let alpha: u8 = (clamped * 255.0).round() as u8;
|
||||
unsafe {
|
||||
let current_ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
||||
let layered_bit = WS_EX_LAYERED.0 as isize;
|
||||
if (current_ex_style & layered_bit) == 0 {
|
||||
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, current_ex_style | layered_bit);
|
||||
}
|
||||
SetLayeredWindowAttributes(hwnd, windows::Win32::Foundation::COLORREF(0), alpha, LWA_ALPHA)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = (window, clamped);
|
||||
Err("window opacity is only implemented on Windows".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a specific click-through state to the overlay (sets ignore_cursor_events,
|
||||
/// performs the opacity pulse, emits the toggled event). Returns the applied state.
|
||||
///
|
||||
@@ -34,14 +77,11 @@ pub async fn apply_state<R: Runtime>(
|
||||
.set_ignore_cursor_events(new_state)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Opacity dip — visual pulse to signal the state change.
|
||||
// Tauri 2.11 does not expose set_opacity on WebviewWindow, so we apply
|
||||
// the dip by evaluating JS on the webview. The JS click-through-toggled
|
||||
// handler is responsible for restoring opacity to the user's actual
|
||||
// target — Rust deliberately does NOT set opacity back, so there is no
|
||||
// possibility of the global-shortcut path "snapping back" to a wrong
|
||||
// value (we don't have a current-opacity getter).
|
||||
let _ = overlay.eval("document.documentElement.style.opacity='0.4'");
|
||||
// Opacity dip — visual pulse to signal the state change. The JS
|
||||
// click-through-toggled handler restores opacity to the user's actual
|
||||
// target after this event fires, so Rust deliberately does NOT set
|
||||
// opacity back here.
|
||||
let _ = apply_window_opacity(&overlay, 0.4);
|
||||
tokio::time::sleep(Duration::from_millis(180)).await;
|
||||
|
||||
app.emit(
|
||||
@@ -105,8 +145,9 @@ pub fn get_hotkey_status(app: AppHandle) -> Result<crate::HotkeyStatus, String>
|
||||
}
|
||||
|
||||
/// Set window opacity. Tauri 2's JS WebviewWindow does NOT expose setOpacity,
|
||||
/// so the JS layer routes through this command. Tauri 2.11 also doesn't expose
|
||||
/// set_opacity on the Rust WebviewWindow, so we apply the change via JS eval.
|
||||
/// so the JS layer routes through this command. Internally we use Win32's
|
||||
/// SetLayeredWindowAttributes — true OS-level window transparency, not a
|
||||
/// CSS shim. See `apply_window_opacity` for details.
|
||||
#[tauri::command]
|
||||
pub fn set_window_opacity<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
@@ -116,9 +157,5 @@ pub fn set_window_opacity<R: Runtime>(
|
||||
let Some(window) = app.get_webview_window(&label) else {
|
||||
return Err(format!("window not found: {label}"));
|
||||
};
|
||||
window
|
||||
.eval(&format!(
|
||||
"document.documentElement.style.opacity='{opacity}'"
|
||||
))
|
||||
.map_err(|e| e.to_string())
|
||||
apply_window_opacity(&window, opacity)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user