feat(overlay): add layer-shell for wayland support (#680)

This commit is contained in:
Soumya Ranjan Patnaik 2026-02-05 16:51:16 +05:30 committed by GitHub
parent 0afd5ec01f
commit 9e538b1ca7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 185 additions and 44 deletions

91
src-tauri/Cargo.lock generated
View file

@ -340,7 +340,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -632,7 +632,7 @@ checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
dependencies = [
"glib-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -739,7 +739,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec 1.15.1",
"target-lexicon",
"target-lexicon 0.12.16",
]
[[package]]
name = "cfg-expr"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a"
dependencies = [
"smallvec 1.15.1",
"target-lexicon 0.13.3",
]
[[package]]
@ -2054,7 +2064,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -2071,7 +2081,7 @@ dependencies = [
"libc",
"pango-sys",
"pkg-config",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -2085,7 +2095,7 @@ dependencies = [
"gobject-sys",
"libc",
"pkg-config",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -2111,7 +2121,7 @@ dependencies = [
"gdk-sys",
"glib-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
"x11",
]
@ -2201,7 +2211,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
"winapi",
]
@ -2249,7 +2259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
dependencies = [
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -2284,7 +2294,7 @@ checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
dependencies = [
"glib-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -2308,6 +2318,34 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "gtk-layer-shell"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc759b3184830a547b31549ab40c4b54450ab702bba79ba23f049bc1d1e3ca98"
dependencies = [
"bitflags 2.10.0",
"gdk",
"glib",
"glib-sys",
"gtk",
"gtk-layer-shell-sys",
"libc",
]
[[package]]
name = "gtk-layer-shell-sys"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4eee067e022416d53a70de69d3d3929d8a6e687f3278b8934faa671750fa6eb"
dependencies = [
"gdk-sys",
"glib-sys",
"gtk-sys",
"libc",
"system-deps 7.0.7",
]
[[package]]
name = "gtk-sys"
version = "0.18.2"
@ -2323,7 +2361,7 @@ dependencies = [
"gobject-sys",
"libc",
"pango-sys",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -2381,6 +2419,8 @@ dependencies = [
"ferrous-opencc",
"flate2",
"futures-util",
"gtk",
"gtk-layer-shell",
"handy-keys",
"hound",
"log",
@ -2976,7 +3016,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -4089,7 +4129,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -5636,7 +5676,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -5993,13 +6033,26 @@ version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"cfg-expr 0.15.8",
"heck 0.5.0",
"pkg-config",
"toml 0.8.2",
"version-compare",
]
[[package]]
name = "system-deps"
version = "7.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
dependencies = [
"cfg-expr 0.20.6",
"heck 0.5.0",
"pkg-config",
"toml 0.9.8",
"version-compare",
]
[[package]]
name = "tao"
version = "0.34.5"
@ -6074,6 +6127,12 @@ version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "target-lexicon"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
[[package]]
name = "tauri"
version = "2.9.1"
@ -7530,7 +7589,7 @@ dependencies = [
"libc",
"pkg-config",
"soup3-sys",
"system-deps",
"system-deps 6.2.2",
]
[[package]]

View file

@ -96,6 +96,10 @@ windows = { version = "0.61.3", features = [
[target.'cfg(target_os = "macos")'.dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2.1" }
[target.'cfg(target_os = "linux")'.dependencies]
gtk-layer-shell = { version = "0.8", features = ["v0_6"] }
gtk = "0.18"
[profile.release]
lto = true
codegen-units = 1

View file

@ -15,6 +15,9 @@ use tauri::WebviewUrl;
#[cfg(target_os = "macos")]
use tauri_nspanel::{tauri_panel, CollectionBehavior, PanelBuilder, PanelLevel};
#[cfg(target_os = "linux")]
use gtk_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
#[cfg(target_os = "macos")]
tauri_panel! {
panel!(RecordingOverlayPanel {
@ -39,6 +42,50 @@ const OVERLAY_BOTTOM_OFFSET: f64 = 15.0;
#[cfg(any(target_os = "windows", target_os = "linux"))]
const OVERLAY_BOTTOM_OFFSET: f64 = 40.0;
#[cfg(target_os = "linux")]
fn update_gtk_layer_shell_anchors(overlay_window: &tauri::webview::WebviewWindow) {
let window_clone = overlay_window.clone();
let _ = overlay_window.run_on_main_thread(move || {
// Try to get the GTK window from the Tauri webview
if let Ok(gtk_window) = window_clone.gtk_window() {
let settings = settings::get_settings(window_clone.app_handle());
match settings.overlay_position {
OverlayPosition::Top => {
gtk_window.set_anchor(Edge::Top, true);
gtk_window.set_anchor(Edge::Bottom, false);
}
OverlayPosition::Bottom | OverlayPosition::None => {
gtk_window.set_anchor(Edge::Bottom, true);
gtk_window.set_anchor(Edge::Top, false);
}
}
}
});
}
/// Initializes GTK layer shell for Linux overlay window
/// Returns true if layer shell was successfully initialized, false otherwise
#[cfg(target_os = "linux")]
fn init_gtk_layer_shell(overlay_window: &tauri::webview::WebviewWindow) -> bool {
if !gtk_layer_shell::is_supported() {
return false;
}
// Try to get the GTK window from the Tauri webview
if let Ok(gtk_window) = overlay_window.gtk_window() {
// Initialize layer shell
gtk_window.init_layer_shell();
gtk_window.set_layer(Layer::Overlay);
gtk_window.set_keyboard_mode(KeyboardMode::None);
gtk_window.set_exclusive_zone(0);
update_gtk_layer_shell_anchors(overlay_window);
return true;
}
false
}
/// Forces a window to be topmost using Win32 API (Windows only)
/// This is more reliable than Tauri's set_always_on_top which can be overridden
#[cfg(target_os = "windows")]
@ -134,35 +181,56 @@ fn calculate_overlay_position(app_handle: &AppHandle) -> Option<(f64, f64)> {
/// Creates the recording overlay window and keeps it hidden by default
#[cfg(not(target_os = "macos"))]
pub fn create_recording_overlay(app_handle: &AppHandle) {
if let Some((x, y)) = calculate_overlay_position(app_handle) {
match WebviewWindowBuilder::new(
app_handle,
"recording_overlay",
tauri::WebviewUrl::App("src/overlay/index.html".into()),
)
.title("Recording")
.position(x, y)
.resizable(false)
.inner_size(OVERLAY_WIDTH, OVERLAY_HEIGHT)
.shadow(false)
.maximizable(false)
.minimizable(false)
.closable(false)
.accept_first_mouse(true)
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.transparent(true)
.focused(false)
.visible(false)
.build()
{
Ok(_window) => {
debug!("Recording overlay window created successfully (hidden)");
}
Err(e) => {
debug!("Failed to create recording overlay window: {}", e);
let position = calculate_overlay_position(app_handle);
// On Linux (Wayland), monitor detection often fails, but we don't need exact coordinates
// for Layer Shell as we use anchors. On other platforms, we require a position.
#[cfg(not(target_os = "linux"))]
if position.is_none() {
debug!("Failed to determine overlay position, not creating overlay window");
return;
}
let mut builder = WebviewWindowBuilder::new(
app_handle,
"recording_overlay",
tauri::WebviewUrl::App("src/overlay/index.html".into()),
)
.title("Recording")
.resizable(false)
.inner_size(OVERLAY_WIDTH, OVERLAY_HEIGHT)
.shadow(false)
.maximizable(false)
.minimizable(false)
.closable(false)
.accept_first_mouse(true)
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.transparent(true)
.focused(false)
.visible(false);
if let Some((x, y)) = position {
builder = builder.position(x, y);
}
match builder.build() {
Ok(window) => {
#[cfg(target_os = "linux")]
{
// Try to initialize GTK layer shell, ignore errors if compositor doesn't support it
if init_gtk_layer_shell(&window) {
debug!("GTK layer shell initialized for overlay window");
} else {
debug!("GTK layer shell not available, falling back to regular window");
}
}
debug!("Recording overlay window created successfully (hidden)");
}
Err(e) => {
debug!("Failed to create recording overlay window: {}", e);
}
}
}
@ -214,6 +282,11 @@ pub fn show_recording_overlay(app_handle: &AppHandle) {
if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") {
// Update position before showing to prevent flicker from position changes
#[cfg(target_os = "linux")]
{
update_gtk_layer_shell_anchors(&overlay_window);
}
if let Some((x, y)) = calculate_overlay_position(app_handle) {
let _ = overlay_window
.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y }));
@ -255,6 +328,11 @@ pub fn show_transcribing_overlay(app_handle: &AppHandle) {
/// Updates the overlay window position based on current settings
pub fn update_overlay_position(app_handle: &AppHandle) {
if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") {
#[cfg(target_os = "linux")]
{
update_gtk_layer_shell_anchors(&overlay_window);
}
if let Some((x, y)) = calculate_overlay_position(app_handle) {
let _ = overlay_window
.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y }));