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-store",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -30,3 +30,11 @@ tokio = { version = "1", features = ["time", "macros"] }
|
|||||||
tauri-plugin-window-state = "2"
|
tauri-plugin-window-state = "2"
|
||||||
tauri-plugin-global-shortcut = "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 std::time::Duration;
|
||||||
|
|
||||||
use serde::Serialize;
|
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 OVERLAY_LABEL: &str = "overlay";
|
||||||
pub const EVT_CLICK_THROUGH_TOGGLED: &str = "click-through-toggled";
|
pub const EVT_CLICK_THROUGH_TOGGLED: &str = "click-through-toggled";
|
||||||
@@ -13,6 +21,41 @@ pub struct ClickThroughToggledPayload {
|
|||||||
pub click_through: bool,
|
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,
|
/// 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.
|
/// 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)
|
.set_ignore_cursor_events(new_state)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Opacity dip — visual pulse to signal the state change.
|
// Opacity dip — visual pulse to signal the state change. The JS
|
||||||
// Tauri 2.11 does not expose set_opacity on WebviewWindow, so we apply
|
// click-through-toggled handler restores opacity to the user's actual
|
||||||
// the dip by evaluating JS on the webview. The JS click-through-toggled
|
// target after this event fires, so Rust deliberately does NOT set
|
||||||
// handler is responsible for restoring opacity to the user's actual
|
// opacity back here.
|
||||||
// target — Rust deliberately does NOT set opacity back, so there is no
|
let _ = apply_window_opacity(&overlay, 0.4);
|
||||||
// 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'");
|
|
||||||
tokio::time::sleep(Duration::from_millis(180)).await;
|
tokio::time::sleep(Duration::from_millis(180)).await;
|
||||||
|
|
||||||
app.emit(
|
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,
|
/// 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
|
/// so the JS layer routes through this command. Internally we use Win32's
|
||||||
/// set_opacity on the Rust WebviewWindow, so we apply the change via JS eval.
|
/// SetLayeredWindowAttributes — true OS-level window transparency, not a
|
||||||
|
/// CSS shim. See `apply_window_opacity` for details.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn set_window_opacity<R: Runtime>(
|
pub fn set_window_opacity<R: Runtime>(
|
||||||
app: AppHandle<R>,
|
app: AppHandle<R>,
|
||||||
@@ -116,9 +157,5 @@ pub fn set_window_opacity<R: Runtime>(
|
|||||||
let Some(window) = app.get_webview_window(&label) else {
|
let Some(window) = app.get_webview_window(&label) else {
|
||||||
return Err(format!("window not found: {label}"));
|
return Err(format!("window not found: {label}"));
|
||||||
};
|
};
|
||||||
window
|
apply_window_opacity(&window, opacity)
|
||||||
.eval(&format!(
|
|
||||||
"document.documentElement.style.opacity='{opacity}'"
|
|
||||||
))
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user