From c549b6edd2e17a77b460ac06d06564a1356b3bd9 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Fri, 17 Jul 2026 04:12:53 +0700 Subject: [PATCH 01/11] fix(overlay): restore Wayland overlay on Tauri 2.11.5 + refactor helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wayland compositors (niri, KWin) only commit a layer surface on an unmapped→mapped transition. On Tauri 2.11.5 the overlay never appeared, and switching its position at runtime dropped it permanently. Linux/Wayland changes (all under cfg(linux) where they touch the surface): - Track gtk-layer-shell init in LAYER_SHELL_ACTIVE and skip layer-shell calls on the non-layer-shell fallback window. - Prime the surface with a show()+hide() cycle so the first real show() maps it fresh instead of being a no-op on an already-mapped surface. - In show_overlay_state, flip Tauri window visibility (overlay_window.show()) so emit("show-overlay") reaches the WebView, and set anchors + show atomically on the GTK thread, synchronizing via a channel so emit runs only after the surface is actually mapped. set_size/set_position stay on the non-layer paths only (niri ignores them on mapped layer surfaces). - Allocate the max overlay size up front on Linux; niri ignores set_size on mapped layer surfaces, so the overlay could not grow with streamed text. - On hide, unmap the GTK surface and flip Tauri visibility so repeated shows keep working. - On runtime position change, remap the surface (unmap → set anchors → map) so the compositor commits the new Top/Bottom anchors. Non-Linux platforms (Windows, macOS) are unchanged from v0.9.3: compact initial size, set_size/set_position, Windows topmost re-assert, and the original timing debug log. Refactor: extract repeated GTK-surface logic into named helpers — pump_gtk_events(), with_gtk_window(), gtk_show_layer_surface(), gtk_remap_layer_surface(), position_overlay_window() — folding the show/hide + main_iteration cycles and run_on_main_thread boilerplate that were duplicated across prime, show_overlay_state, update_overlay_position and hide_recording_overlay. Behavior unchanged. bindings.ts is intentionally left as the v0.9.3 generated content. --- src-tauri/src/overlay.rs | 305 ++++++++++++++++++++++++++++++--------- 1 file changed, 235 insertions(+), 70 deletions(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index c4e4be1f8e..d4d426e227 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -2,7 +2,8 @@ use crate::input; use crate::settings; use crate::settings::{OverlayPosition, OverlayStyle}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::sync::mpsc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize}; #[cfg(not(target_os = "macos"))] @@ -73,23 +74,87 @@ 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 set_layer_shell_anchors(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { + match position { + OverlayPosition::Top => { + gtk_window.set_anchor(Edge::Top, true); + gtk_window.set_anchor(Edge::Bottom, false); + } + OverlayPosition::Bottom => { + gtk_window.set_anchor(Edge::Bottom, true); + gtk_window.set_anchor(Edge::Top, false); + } + } +} + +/// Runs the GTK main loop until all pending events are processed, so a surface +/// commit (map/unmap) requested just before is actually sent to the compositor. +#[cfg(target_os = "linux")] +fn pump_gtk_events() { + while gtk::events_pending() { + gtk::main_iteration(); + } +} + +/// Runs `f` with the GTK window of `overlay_window` on the GTK main thread. +/// No-op if the GTK window isn't available. Linux only. +#[cfg(target_os = "linux")] +fn with_gtk_window(overlay_window: &tauri::webview::WebviewWindow, f: F) +where + F: FnOnce(>k::ApplicationWindow) + Send + 'static, +{ + let w = overlay_window.clone(); + let _ = overlay_window.run_on_main_thread(move || { + if let Ok(gtk_window) = w.gtk_window() { + f(>k_window); + } + }); +} + +/// Shows a layer surface: apply anchors, map it, pump the GTK event loop so the +/// compositor commits it. Used by the prime cycle and show_overlay_state. +#[cfg(target_os = "linux")] +fn gtk_show_layer_surface(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { + use gtk::prelude::WidgetExt; + set_layer_shell_anchors(gtk_window, position); + gtk_window.show(); + pump_gtk_events(); +} + +/// Remaps a layer surface so a runtime anchor change is committed: unmap, apply +/// new anchors, map again. Used by update_overlay_position on position switch. +#[cfg(target_os = "linux")] +fn gtk_remap_layer_surface(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { + use gtk::prelude::WidgetExt; + gtk_window.hide(); + pump_gtk_events(); + set_layer_shell_anchors(gtk_window, position); + gtk_window.show(); + pump_gtk_events(); +} + +/// Centers a regular (non-layer-shell) overlay window using its current size. +fn position_overlay_window(overlay_window: &tauri::webview::WebviewWindow, app_handle: &AppHandle) { + let (width, height) = + current_overlay_logical_size(overlay_window).unwrap_or((OVERLAY_WIDTH, OVERLAY_HEIGHT)); + if let Some((x, y)) = calculate_overlay_position(app_handle, width, height) { + let _ = + overlay_window.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); + } +} + #[cfg(target_os = "linux")] fn update_gtk_layer_shell_anchors(overlay_window: &tauri::webview::WebviewWindow) { + if !LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { + return; + } 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 => { - gtk_window.set_anchor(Edge::Bottom, true); - gtk_window.set_anchor(Edge::Top, false); - } - } + set_layer_shell_anchors(>k_window, settings.overlay_position); } }); } @@ -119,22 +184,42 @@ fn init_gtk_layer_shell(overlay_window: &tauri::webview::WebviewWindow) -> bool } if !gtk_layer_shell::is_supported() { + debug!( + "Layer shell not supported by compositor (GDK_BACKEND={:?}, XDG_SESSION_TYPE={:?})", + env::var("GDK_BACKEND"), + env::var("XDG_SESSION_TYPE"), + ); 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; + match overlay_window.gtk_window() { + Ok(gtk_window) => { + use gtk::prelude::WidgetExt; + let realized = gtk_window.is_realized(); + + 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); + + let ok = gtk_window.is_layer_window(); + debug!( + "Layer shell init: realized_before_init={} is_layer_window={}", + realized, ok + ); + if !ok { + log::error!("Failed to init layer shell (window was already realized)"); + } + LAYER_SHELL_ACTIVE.store(ok, Ordering::SeqCst); + ok + } + Err(e) => { + log::error!("gtk_window() failed: {:?}", e); + false + } } - false } /// Forces a window to be topmost using Win32 API (Windows only) @@ -288,6 +373,14 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { // Position starts unset — update_overlay_position() sets the correct // LogicalPosition before the overlay is shown. + // On Linux/Wayland allocate the largest possible size so gtk-layer-shell + // surfaces never need resizing (compositors like niri ignore set_size on + // mapped layer surfaces); other platforms keep the compact initial size. + #[cfg(target_os = "linux")] + let (init_w, init_h) = (OVERLAY_STREAM_WIDTH, OVERLAY_STREAM_HEIGHT); + #[cfg(not(target_os = "linux"))] + let (init_w, init_h) = (OVERLAY_WIDTH, OVERLAY_HEIGHT); + let mut builder = WebviewWindowBuilder::new( app_handle, "recording_overlay", @@ -295,7 +388,7 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { ) .title("Recording") .resizable(false) - .inner_size(OVERLAY_WIDTH, OVERLAY_HEIGHT) + .inner_size(init_w, init_h) .shadow(false) .maximizable(false) .minimizable(false) @@ -318,9 +411,30 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { 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"); + + // Prime the layer surface with a full map→unmap cycle so the + // first real show() on the first transcription is honored. + // Wayland compositors (niri, KWin) only commit a layer surface + // on an unmapped→mapped transition; priming with show()+hide() + // leaves the surface unmapped, so the first real show() maps it + // fresh instead of being a no-op on an already-mapped surface. + let (tx, rx) = mpsc::channel(); + let w = window.clone(); + let _ = window.run_on_main_thread(move || { + if let Ok(gtk_window) = w.gtk_window() { + use gtk::prelude::WidgetExt; + let s = settings::get_settings(w.app_handle()); + set_layer_shell_anchors(>k_window, s.overlay_position); + gtk_window.show(); + pump_gtk_events(); + gtk_window.hide(); + pump_gtk_events(); + } + let _ = tx.send(()); + }); + let _ = rx.recv_timeout(Duration::from_millis(500)); } else { debug!("GTK layer shell not available, falling back to regular window"); } @@ -373,50 +487,79 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { } fn show_overlay_state(app_handle: &AppHandle, state: &str) { - // Whether the overlay shows at all is governed by overlay_style; position - // only chooses Top vs Bottom placement. let settings = settings::get_settings(app_handle); if settings.overlay_style == OverlayStyle::None { return; } - // Size the overlay for this state (compact vs. streaming), then position it. let (width, height) = overlay_dimensions(state); if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") { #[cfg(target_os = "linux")] - update_gtk_layer_shell_anchors(&overlay_window); - - let size_started = std::time::Instant::now(); - let _ = overlay_window.set_size(tauri::Size::Logical(tauri::LogicalSize { width, height })); - let size_elapsed = size_started.elapsed(); - - let pos_started = std::time::Instant::now(); - let mut set_pos_elapsed = std::time::Duration::ZERO; - if let Some((x, y)) = calculate_overlay_position(app_handle, width, height) { - let set_pos_started = std::time::Instant::now(); - let _ = overlay_window - .set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); - set_pos_elapsed = set_pos_started.elapsed(); + if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { + // Layer-shell on niri: set anchors + show atomically on the GTK + // main thread. Calling show() separately from anchor setup meant + // the surface was committed before anchors were applied, making it + // invisible. The Tauri window visibility must also flip to "shown" + // so `emit("show-overlay")` is delivered to the WebView. + let _ = overlay_window.show(); + let pos = settings::get_settings(overlay_window.app_handle()).overlay_position; + // Synchronize with the GTK main thread so emit("show-overlay") below + // runs only after the surface is actually mapped. Without this, the + // event reaches the WebView before the layer surface is visible, + // racing the fade-in and any first-frame layout. + let (tx, rx) = mpsc::channel(); + let w = overlay_window.clone(); + let _ = overlay_window.run_on_main_thread(move || { + if let Ok(gtk_window) = w.gtk_window() { + gtk_show_layer_surface(>k_window, pos); + } + let _ = tx.send(()); + }); + let _ = rx.recv_timeout(Duration::from_millis(500)); + } else { + // Non-layer-shell fallback (regular window) on Linux. + let _ = overlay_window.show(); + let _ = + overlay_window.set_size(tauri::Size::Logical(tauri::LogicalSize { width, height })); + position_overlay_window(&overlay_window, app_handle); } - let pos_calc_elapsed = pos_started.elapsed() - set_pos_elapsed; - - let show_started = std::time::Instant::now(); - let _ = overlay_window.show(); - let show_elapsed = show_started.elapsed(); - // On Windows, aggressively re-assert "topmost" in the native Z-order after showing - #[cfg(target_os = "windows")] - force_overlay_topmost(&overlay_window); + #[cfg(not(target_os = "linux"))] + { + let size_started = std::time::Instant::now(); + let _ = + overlay_window.set_size(tauri::Size::Logical(tauri::LogicalSize { width, height })); + let size_elapsed = size_started.elapsed(); + + let pos_started = std::time::Instant::now(); + let mut set_pos_elapsed = std::time::Duration::ZERO; + if let Some((x, y)) = calculate_overlay_position(app_handle, width, height) { + let set_pos_started = std::time::Instant::now(); + let _ = overlay_window + .set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); + set_pos_elapsed = set_pos_started.elapsed(); + } + let pos_calc_elapsed = pos_started.elapsed() - set_pos_elapsed; + + let show_started = std::time::Instant::now(); + let _ = overlay_window.show(); + let show_elapsed = show_started.elapsed(); + + // On Windows, aggressively re-assert "topmost" in the native Z-order after showing + #[cfg(target_os = "windows")] + force_overlay_topmost(&overlay_window); + + log::debug!( + "overlay '{}': set_size={:?} pos_calc={:?} set_pos={:?} show={:?}", + state, + size_elapsed, + pos_calc_elapsed, + set_pos_elapsed, + show_elapsed + ); + } let _ = overlay_window.emit("show-overlay", state); - log::debug!( - "overlay '{}': set_size={:?} pos_calc={:?} set_pos={:?} show={:?}", - state, - size_elapsed, - pos_calc_elapsed, - set_pos_elapsed, - show_elapsed - ); } } @@ -444,32 +587,50 @@ pub fn show_processing_overlay(app_handle: &AppHandle) { 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 LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { + // On Wayland layer surfaces, anchors can only be applied on an + // unmapped→mapped transition. To change Top/Bottom at runtime we + // remap the surface: unmap, set the new anchors, then map again so + // the compositor (niri, KWin) commits them. Skipping the remap + // leaves the surface mapped with stale anchors and the overlay + // disappears on position change. + let pos = settings::get_settings(overlay_window.app_handle()).overlay_position; + with_gtk_window(&overlay_window, move |gtk_window| { + gtk_remap_layer_surface(gtk_window, pos); + }); + // Keep Tauri's visibility state in sync (like hide_recording_overlay). + let _ = overlay_window.hide(); + return; } - // Use the window's current size so centering stays correct whether the - // overlay is in compact or streaming layout. - let (width, height) = current_overlay_logical_size(&overlay_window) - .unwrap_or((OVERLAY_WIDTH, OVERLAY_HEIGHT)); - if let Some((x, y)) = calculate_overlay_position(app_handle, width, height) { - let _ = overlay_window - .set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); + #[cfg(target_os = "linux")] + if !LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { + // Non-layer-shell fallback on Linux: anchors don't apply, just move. + position_overlay_window(&overlay_window, app_handle); } + + #[cfg(not(target_os = "linux"))] + position_overlay_window(&overlay_window, app_handle); } } /// Hides the recording overlay window with fade-out animation pub fn hide_recording_overlay(app_handle: &AppHandle) { - // Always hide the overlay regardless of settings - if setting was changed while recording, - // we still want to hide it properly + #[cfg(target_os = "linux")] + use gtk::prelude::WidgetExt; if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") { - // Emit event to trigger fade-out animation let _ = overlay_window.emit("hide-overlay", ()); - // Hide the window after a short delay to allow animation to complete let window_clone = overlay_window.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(300)); + #[cfg(target_os = "linux")] + if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { + with_gtk_window(&window_clone, |gtk_window| gtk_window.hide()); + // Keep Tauri's visibility state in sync so the next + // `overlay_window.show()` (which re-flips the flag) is honored. + let _ = window_clone.hide(); + } + #[cfg(not(target_os = "linux"))] let _ = window_clone.hide(); }); } @@ -481,6 +642,10 @@ pub fn hide_recording_overlay(app_handle: &AppHandle) { // populates the cache from initial settings. static OVERLAY_ENABLED: AtomicBool = AtomicBool::new(false); +/// Tracks whether gtk-layer-shell was successfully initialized (Linux only). +/// Used to skip layer-shell calls when the window is a regular fallback. +static LAYER_SHELL_ACTIVE: AtomicBool = AtomicBool::new(false); + /// Update the cached overlay-enabled flag. Called from `lib.rs` at /// startup after settings load, and from `change_overlay_style_setting` /// whenever the user changes whether the overlay is shown. From 92f361b33366c0f10fe32253a61f9a1d83618644 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 02:31:26 +0700 Subject: [PATCH 02/11] fix(overlay): avoid hiding/flashing Wayland overlay on position change On Wayland layer-shell, only remap the surface when it is currently mapped; for an unmapped surface just set the anchors so they apply on the next map. This keeps Tauri's window visibility untouched, so a mid-recording position switch no longer hides the overlay and an idle switch no longer flashes it. --- src-tauri/src/overlay.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index d4d426e227..651c6483ce 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -589,17 +589,23 @@ pub fn update_overlay_position(app_handle: &AppHandle) { #[cfg(target_os = "linux")] if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { // On Wayland layer surfaces, anchors can only be applied on an - // unmapped→mapped transition. To change Top/Bottom at runtime we - // remap the surface: unmap, set the new anchors, then map again so - // the compositor (niri, KWin) commits them. Skipping the remap - // leaves the surface mapped with stale anchors and the overlay - // disappears on position change. + // unmapped→mapped transition. A mapped surface must be remapped + // (unmap, set the new anchors, map again) so the compositor (niri, + // KWin) commits them; skipping the remap leaves stale anchors and + // the overlay disappears on position change. An unmapped surface + // needs no remap — new anchors apply on its next map. Deciding on + // the GTK thread by the mapped state keeps the window's visibility + // untouched, so a mid-recording switch no longer hides the overlay + // and an idle switch no longer flashes it. let pos = settings::get_settings(overlay_window.app_handle()).overlay_position; with_gtk_window(&overlay_window, move |gtk_window| { - gtk_remap_layer_surface(gtk_window, pos); + use gtk::prelude::WidgetExt; + if gtk_window.is_mapped() { + gtk_remap_layer_surface(gtk_window, pos); + } else { + set_layer_shell_anchors(gtk_window, pos); + } }); - // Keep Tauri's visibility state in sync (like hide_recording_overlay). - let _ = overlay_window.hide(); return; } From 5b19c3d40647447bfc94c5d69b5b30d7d33da046 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 20:52:14 +0700 Subject: [PATCH 03/11] refactor(overlay): remove dead update_gtk_layer_shell_anchors function The function was only called from init_gtk_layer_shell, but its guard always triggered there because LAYER_SHELL_ACTIVE is stored later in the same function. Initial anchors are already set by the priming cycle, and subsequent shows re-apply anchors via gtk_show_layer_surface. No behavior change. --- src-tauri/src/overlay.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 651c6483ce..2a0e711e0d 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -144,21 +144,6 @@ fn position_overlay_window(overlay_window: &tauri::webview::WebviewWindow, app_h } } -#[cfg(target_os = "linux")] -fn update_gtk_layer_shell_anchors(overlay_window: &tauri::webview::WebviewWindow) { - if !LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { - return; - } - 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()); - set_layer_shell_anchors(>k_window, settings.overlay_position); - } - }); -} - /// Returns true when the environment variable is set to a truthy value /// (e.g. "1", "true", "yes", "on"). /// "0", "false", "no", "off" and empty string are treated as falsy (case-insensitive). @@ -202,8 +187,6 @@ fn init_gtk_layer_shell(overlay_window: &tauri::webview::WebviewWindow) -> bool gtk_window.set_keyboard_mode(KeyboardMode::None); gtk_window.set_exclusive_zone(0); - update_gtk_layer_shell_anchors(overlay_window); - let ok = gtk_window.is_layer_window(); debug!( "Layer shell init: realized_before_init={} is_layer_window={}", From 29155086f56317382b053458ad20525802f0ee50 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 20:55:32 +0700 Subject: [PATCH 04/11] fix(overlay): avoid flash at default position on Linux fallback show --- src-tauri/src/overlay.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 2a0e711e0d..41177af78c 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -500,11 +500,13 @@ fn show_overlay_state(app_handle: &AppHandle, state: &str) { }); let _ = rx.recv_timeout(Duration::from_millis(500)); } else { - // Non-layer-shell fallback (regular window) on Linux. - let _ = overlay_window.show(); + // Non-layer-shell fallback (regular window) on Linux: size and position + // the window before showing it so it doesn't flash at the default or + // stale position. let _ = overlay_window.set_size(tauri::Size::Logical(tauri::LogicalSize { width, height })); position_overlay_window(&overlay_window, app_handle); + let _ = overlay_window.show(); } #[cfg(not(target_os = "linux"))] From 2e22ee85a4ffbd3355a3e0d3659023a5d6ee7bef Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 20:55:59 +0700 Subject: [PATCH 05/11] refactor(overlay): gate Linux-only imports and static behind cfg --- src-tauri/src/overlay.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 41177af78c..90ffdac75d 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -2,8 +2,11 @@ use crate::input; use crate::settings; use crate::settings::{OverlayPosition, OverlayStyle}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +#[cfg(target_os = "linux")] use std::sync::mpsc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(target_os = "linux")] +use std::time::Duration; +use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize}; #[cfg(not(target_os = "macos"))] @@ -635,6 +638,7 @@ static OVERLAY_ENABLED: AtomicBool = AtomicBool::new(false); /// Tracks whether gtk-layer-shell was successfully initialized (Linux only). /// Used to skip layer-shell calls when the window is a regular fallback. +#[cfg(target_os = "linux")] static LAYER_SHELL_ACTIVE: AtomicBool = AtomicBool::new(false); /// Update the cached overlay-enabled flag. Called from `lib.rs` at From b75ebf8b0a72e896fd847a512cfa0431f990ef08 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 20:56:16 +0700 Subject: [PATCH 06/11] refactor(overlay): reuse settings overlay position in show_overlay_state --- src-tauri/src/overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 90ffdac75d..e47dbebcb5 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -488,7 +488,7 @@ fn show_overlay_state(app_handle: &AppHandle, state: &str) { // invisible. The Tauri window visibility must also flip to "shown" // so `emit("show-overlay")` is delivered to the WebView. let _ = overlay_window.show(); - let pos = settings::get_settings(overlay_window.app_handle()).overlay_position; + let pos = settings.overlay_position; // Synchronize with the GTK main thread so emit("show-overlay") below // runs only after the surface is actually mapped. Without this, the // event reaches the WebView before the layer surface is visible, From b15f156ea2ce958ee1f125f687b38e8d52692328 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 20:56:33 +0700 Subject: [PATCH 07/11] docs(overlay): correct gtk_show_layer_surface doc comment --- src-tauri/src/overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index e47dbebcb5..43e2ac96e1 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -116,7 +116,7 @@ where } /// Shows a layer surface: apply anchors, map it, pump the GTK event loop so the -/// compositor commits it. Used by the prime cycle and show_overlay_state. +/// compositor commits it. Used by show_overlay_state. #[cfg(target_os = "linux")] fn gtk_show_layer_surface(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { use gtk::prelude::WidgetExt; From b93434e6a6644eba4846b1345d970060ec498921 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 20:56:52 +0700 Subject: [PATCH 08/11] refactor(overlay): soften init layer shell error message --- src-tauri/src/overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 43e2ac96e1..f4bbeb7e1c 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -196,7 +196,7 @@ fn init_gtk_layer_shell(overlay_window: &tauri::webview::WebviewWindow) -> bool realized, ok ); if !ok { - log::error!("Failed to init layer shell (window was already realized)"); + log::error!("Failed to init layer shell"); } LAYER_SHELL_ACTIVE.store(ok, Ordering::SeqCst); ok From b7f2fa19aa53162b3a670cb8d434db4834bb34be Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Mon, 20 Jul 2026 21:29:13 +0700 Subject: [PATCH 09/11] docs(clamshell): unify is_laptop doc comments across platforms --- src-tauri/src/helpers/clamshell.rs | 14 +++++++++----- src/bindings.ts | 7 ++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/helpers/clamshell.rs b/src-tauri/src/helpers/clamshell.rs index 4f064055db..e8ff681ab0 100644 --- a/src-tauri/src/helpers/clamshell.rs +++ b/src-tauri/src/helpers/clamshell.rs @@ -25,10 +25,11 @@ pub fn is_clamshell() -> Result { Ok(stdout.contains("\"AppleClamshellState\" = Yes")) } -/// Checks if the Mac is a laptop by detecting battery presence +/// Checks if the Mac is a laptop by detecting battery presence. /// -/// This uses pmset to check for battery information. -/// Returns true if a battery is detected (laptop), false otherwise (desktop) +/// This uses pmset to check for battery information on macOS. +/// Returns true if a battery is detected (laptop), false otherwise (desktop). +/// On non-macOS platforms this is a stub that always returns false. #[cfg(target_os = "macos")] #[tauri::command] #[specta::specta] @@ -52,8 +53,11 @@ pub fn is_clamshell() -> Result { Ok(false) } -/// Stub implementation for non-macOS platforms -/// Always returns false since laptop detection is macOS-specific +/// Checks if the Mac is a laptop by detecting battery presence. +/// +/// This uses pmset to check for battery information on macOS. +/// Returns true if a battery is detected (laptop), false otherwise (desktop). +/// On non-macOS platforms this is a stub that always returns false. #[cfg(not(target_os = "macos"))] #[tauri::command] #[specta::specta] diff --git a/src/bindings.ts b/src/bindings.ts index f31c730a3b..11e806a58d 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -846,10 +846,11 @@ async updateRecordingRetentionPeriod(period: string) : Promise> { try { From 5b93cb50183758f168785645b18beb60c12df479 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Fri, 7 Aug 2026 14:52:09 +0800 Subject: [PATCH 10/11] fix(overlay): size Wayland layer surfaces through GTK --- src-tauri/src/helpers/clamshell.rs | 14 +- src-tauri/src/overlay.rs | 301 ++++++++++------------------- src/bindings.ts | 7 +- 3 files changed, 111 insertions(+), 211 deletions(-) diff --git a/src-tauri/src/helpers/clamshell.rs b/src-tauri/src/helpers/clamshell.rs index e8ff681ab0..4f064055db 100644 --- a/src-tauri/src/helpers/clamshell.rs +++ b/src-tauri/src/helpers/clamshell.rs @@ -25,11 +25,10 @@ pub fn is_clamshell() -> Result { Ok(stdout.contains("\"AppleClamshellState\" = Yes")) } -/// Checks if the Mac is a laptop by detecting battery presence. +/// Checks if the Mac is a laptop by detecting battery presence /// -/// This uses pmset to check for battery information on macOS. -/// Returns true if a battery is detected (laptop), false otherwise (desktop). -/// On non-macOS platforms this is a stub that always returns false. +/// This uses pmset to check for battery information. +/// Returns true if a battery is detected (laptop), false otherwise (desktop) #[cfg(target_os = "macos")] #[tauri::command] #[specta::specta] @@ -53,11 +52,8 @@ pub fn is_clamshell() -> Result { Ok(false) } -/// Checks if the Mac is a laptop by detecting battery presence. -/// -/// This uses pmset to check for battery information on macOS. -/// Returns true if a battery is detected (laptop), false otherwise (desktop). -/// On non-macOS platforms this is a stub that always returns false. +/// Stub implementation for non-macOS platforms +/// Always returns false since laptop detection is macOS-specific #[cfg(not(target_os = "macos"))] #[tauri::command] #[specta::specta] diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 161b40158f..07498213ed 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -2,10 +2,6 @@ use crate::input; use crate::settings; use crate::settings::{OverlayPosition, OverlayStyle}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -#[cfg(target_os = "linux")] -use std::sync::mpsc; -#[cfg(target_os = "linux")] -use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, PhysicalSize}; @@ -77,75 +73,55 @@ const OVERLAY_BOTTOM_OFFSET: f64 = 15.0; #[cfg(any(target_os = "windows", target_os = "linux"))] const OVERLAY_BOTTOM_OFFSET: f64 = 40.0; +/// Configures the edge and offset of a GTK layer surface. gtk-layer-shell +/// commits anchor and margin changes itself, including while the surface is +/// mapped, so changing position does not require a manual hide/show cycle. #[cfg(target_os = "linux")] -fn set_layer_shell_anchors(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { - match position { - OverlayPosition::Top => { - gtk_window.set_anchor(Edge::Top, true); - gtk_window.set_anchor(Edge::Bottom, false); - } - OverlayPosition::Bottom => { - gtk_window.set_anchor(Edge::Bottom, true); - gtk_window.set_anchor(Edge::Top, false); - } - } -} - -/// Runs the GTK main loop until all pending events are processed, so a surface -/// commit (map/unmap) requested just before is actually sent to the compositor. -#[cfg(target_os = "linux")] -fn pump_gtk_events() { - while gtk::events_pending() { - gtk::main_iteration(); - } -} +fn configure_layer_shell_position(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { + let (edge, opposite_edge, margin) = match position { + OverlayPosition::Top => (Edge::Top, Edge::Bottom, OVERLAY_TOP_OFFSET), + OverlayPosition::Bottom => (Edge::Bottom, Edge::Top, OVERLAY_BOTTOM_OFFSET), + }; -/// Runs `f` with the GTK window of `overlay_window` on the GTK main thread. -/// No-op if the GTK window isn't available. Linux only. -#[cfg(target_os = "linux")] -fn with_gtk_window(overlay_window: &tauri::webview::WebviewWindow, f: F) -where - F: FnOnce(>k::ApplicationWindow) + Send + 'static, -{ - let w = overlay_window.clone(); - let _ = overlay_window.run_on_main_thread(move || { - if let Ok(gtk_window) = w.gtk_window() { - f(>k_window); - } - }); + gtk_window.set_anchor(edge, true); + gtk_window.set_anchor(opposite_edge, false); + gtk_window.set_layer_shell_margin(edge, margin.round() as i32); + gtk_window.set_layer_shell_margin(opposite_edge, 0); } -/// Shows a layer surface: apply anchors, map it, pump the GTK event loop so the -/// compositor commits it. Used by show_overlay_state. +/// Configures a GTK layer surface before it is shown. +/// +/// Tauri's normal `set_size` path calls `gtk_window_resize`, but layer surfaces +/// derive their dimensions from GTK's size request. gtk-layer-shell documents +/// the `set_size_request` + `resize(1, 1)` sequence for forcing a new size. #[cfg(target_os = "linux")] -fn gtk_show_layer_surface(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { - use gtk::prelude::WidgetExt; - set_layer_shell_anchors(gtk_window, position); - gtk_window.show(); - pump_gtk_events(); -} +fn configure_layer_shell_surface( + gtk_window: >k::ApplicationWindow, + app_handle: &AppHandle, + position: OverlayPosition, + width: f64, + height: f64, +) { + use gtk::prelude::{GtkWindowExt, WidgetExt}; -/// Remaps a layer surface so a runtime anchor change is committed: unmap, apply -/// new anchors, map again. Used by update_overlay_position on position switch. -#[cfg(target_os = "linux")] -fn gtk_remap_layer_surface(gtk_window: >k::ApplicationWindow, position: OverlayPosition) { - use gtk::prelude::WidgetExt; - gtk_window.hide(); - pump_gtk_events(); - set_layer_shell_anchors(gtk_window, position); - gtk_window.show(); - pump_gtk_events(); -} + configure_layer_shell_position(gtk_window, position); -/// Centers a regular (non-layer-shell) overlay window using its current size. -#[cfg(not(target_os = "windows"))] -fn position_overlay_window(overlay_window: &tauri::webview::WebviewWindow, app_handle: &AppHandle) { - let (width, height) = - current_overlay_logical_size(overlay_window).unwrap_or((OVERLAY_WIDTH, OVERLAY_HEIGHT)); - if let Some((x, y)) = calculate_overlay_position(app_handle, width, height) { - let _ = - overlay_window.set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); + // Prefer the output containing the cursor when global cursor coordinates + // are available. On restricted Wayland compositors Enigo may not expose + // them; leaving the monitor unset lets the compositor choose instead. + if let Some((x, y)) = input::get_cursor_position(app_handle) { + if let Some(monitor) = + gtk::gdk::Display::default().and_then(|display| display.monitor_at_point(x, y)) + { + gtk_window.set_monitor(&monitor); + } } + + gtk_window.set_size_request( + width.round().max(1.0) as i32, + height.round().max(1.0) as i32, + ); + gtk_window.resize(1, 1); } /// Returns true when the environment variable is set to a truthy value @@ -173,40 +149,30 @@ fn init_gtk_layer_shell(overlay_window: &tauri::webview::WebviewWindow) -> bool } if !gtk_layer_shell::is_supported() { - debug!( - "Layer shell not supported by compositor (GDK_BACKEND={:?}, XDG_SESSION_TYPE={:?})", - env::var("GDK_BACKEND"), - env::var("XDG_SESSION_TYPE"), - ); return false; } - match overlay_window.gtk_window() { - Ok(gtk_window) => { - use gtk::prelude::WidgetExt; - let realized = gtk_window.is_realized(); - - gtk_window.init_layer_shell(); - gtk_window.set_layer(Layer::Overlay); - gtk_window.set_keyboard_mode(KeyboardMode::None); - gtk_window.set_exclusive_zone(0); + // Try to get the GTK window from the Tauri webview + if let Ok(gtk_window) = overlay_window.gtk_window() { + gtk_window.init_layer_shell(); + gtk_window.set_layer(Layer::Overlay); + gtk_window.set_keyboard_mode(KeyboardMode::None); + gtk_window.set_exclusive_zone(0); + + let overlay_position = settings::get_settings(overlay_window.app_handle()).overlay_position; + configure_layer_shell_surface( + >k_window, + overlay_window.app_handle(), + overlay_position, + OVERLAY_WIDTH, + OVERLAY_HEIGHT, + ); - let ok = gtk_window.is_layer_window(); - debug!( - "Layer shell init: realized_before_init={} is_layer_window={}", - realized, ok - ); - if !ok { - log::error!("Failed to init layer shell"); - } - LAYER_SHELL_ACTIVE.store(ok, Ordering::SeqCst); - ok - } - Err(e) => { - log::error!("gtk_window() failed: {:?}", e); - false - } + let initialized = gtk_window.is_layer_window(); + LAYER_SHELL_ACTIVE.store(initialized, Ordering::SeqCst); + return initialized; } + false } /// Forces a window to be topmost using Win32 API (Windows only) @@ -446,14 +412,6 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { // Position starts unset — update_overlay_position() sets the correct // LogicalPosition before the overlay is shown. - // On Linux/Wayland allocate the largest possible size so gtk-layer-shell - // surfaces never need resizing (compositors like niri ignore set_size on - // mapped layer surfaces); other platforms keep the compact initial size. - #[cfg(target_os = "linux")] - let (init_w, init_h) = (OVERLAY_STREAM_WIDTH, OVERLAY_STREAM_HEIGHT); - #[cfg(not(target_os = "linux"))] - let (init_w, init_h) = (OVERLAY_WIDTH, OVERLAY_HEIGHT); - let mut builder = WebviewWindowBuilder::new( app_handle, "recording_overlay", @@ -461,7 +419,7 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { ) .title("Recording") .resizable(false) - .inner_size(init_w, init_h) + .inner_size(OVERLAY_WIDTH, OVERLAY_HEIGHT) .shadow(false) .maximizable(false) .minimizable(false) @@ -484,30 +442,9 @@ pub fn create_recording_overlay(app_handle: &AppHandle) { 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"); - - // Prime the layer surface with a full map→unmap cycle so the - // first real show() on the first transcription is honored. - // Wayland compositors (niri, KWin) only commit a layer surface - // on an unmapped→mapped transition; priming with show()+hide() - // leaves the surface unmapped, so the first real show() maps it - // fresh instead of being a no-op on an already-mapped surface. - let (tx, rx) = mpsc::channel(); - let w = window.clone(); - let _ = window.run_on_main_thread(move || { - if let Ok(gtk_window) = w.gtk_window() { - use gtk::prelude::WidgetExt; - let s = settings::get_settings(w.app_handle()); - set_layer_shell_anchors(>k_window, s.overlay_position); - gtk_window.show(); - pump_gtk_events(); - gtk_window.hide(); - pump_gtk_events(); - } - let _ = tx.send(()); - }); - let _ = rx.recv_timeout(Duration::from_millis(500)); } else { debug!("GTK layer shell not available, falling back to regular window"); } @@ -586,39 +523,23 @@ fn show_overlay_state_on_main(app_handle: &AppHandle, state: &str) { let (width, height) = overlay_dimensions(state); if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") { #[cfg(target_os = "linux")] - if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { - // Layer-shell on niri: set anchors + show atomically on the GTK - // main thread. Calling show() separately from anchor setup meant - // the surface was committed before anchors were applied, making it - // invisible. The Tauri window visibility must also flip to "shown" - // so `emit("show-overlay")` is delivered to the WebView. - let _ = overlay_window.show(); - let pos = settings::get_settings(app_handle).overlay_position; - // Synchronize with the GTK main thread so emit("show-overlay") below - // runs only after the surface is actually mapped. Without this, the - // event reaches the WebView before the layer surface is visible, - // racing the fade-in and any first-frame layout. - let (tx, rx) = mpsc::channel(); - let w = overlay_window.clone(); - let _ = overlay_window.run_on_main_thread(move || { - if let Ok(gtk_window) = w.gtk_window() { - gtk_show_layer_surface(>k_window, pos); + let shown_with_layer_shell = if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { + let position = settings::get_settings(app_handle).overlay_position; + match overlay_window.gtk_window() { + Ok(gtk_window) => { + configure_layer_shell_surface(>k_window, app_handle, position, width, height) } - let _ = tx.send(()); - }); - let _ = rx.recv_timeout(Duration::from_millis(500)); - } else { - // Non-layer-shell fallback (regular window) on Linux: size and position - // the window before showing it so it doesn't flash at the default or - // stale position. - let _ = - overlay_window.set_size(tauri::Size::Logical(tauri::LogicalSize { width, height })); - position_overlay_window(&overlay_window, app_handle); + Err(error) => log::error!("Failed to access GTK overlay window: {error}"), + } let _ = overlay_window.show(); - } - + true + } else { + false + }; #[cfg(not(target_os = "linux"))] - { + let shown_with_layer_shell = false; + + if !shown_with_layer_shell { let size_started = std::time::Instant::now(); #[cfg(not(target_os = "windows"))] let _ = @@ -658,6 +579,13 @@ fn show_overlay_state_on_main(app_handle: &AppHandle, state: &str) { #[cfg(target_os = "windows")] force_overlay_topmost(&overlay_window); + // Re-assert bounds after show(): the pre-show move crosses the DPI + // boundary, and tao's WM_DPICHANGED reflow clobbers the first placement. + #[cfg(target_os = "windows")] + if let Err(error) = place_windows_overlay(app_handle, &overlay_window, width, height) { + log::error!("Failed to re-assert recording overlay position: {error}"); + } + log::debug!( "overlay '{}': set_size={:?} pos_calc={:?} set_pos={:?} show={:?}", state, @@ -668,13 +596,6 @@ fn show_overlay_state_on_main(app_handle: &AppHandle, state: &str) { ); } - // Re-assert bounds after show(): the pre-show move crosses the DPI - // boundary, and tao's WM_DPICHANGED reflow clobbers the first placement. - #[cfg(target_os = "windows")] - if let Err(error) = place_windows_overlay(app_handle, &overlay_window, width, height) { - log::error!("Failed to re-assert recording overlay position: {error}"); - } - let _ = overlay_window.emit("show-overlay", state); } } @@ -711,33 +632,14 @@ fn update_overlay_position_on_main(app_handle: &AppHandle) { if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") { #[cfg(target_os = "linux")] if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { - // On Wayland layer surfaces, anchors can only be applied on an - // unmapped→mapped transition. A mapped surface must be remapped - // (unmap, set the new anchors, map again) so the compositor (niri, - // KWin) commits them; skipping the remap leaves stale anchors and - // the overlay disappears on position change. An unmapped surface - // needs no remap — new anchors apply on its next map. Deciding on - // the GTK thread by the mapped state keeps the window's visibility - // untouched, so a mid-recording switch no longer hides the overlay - // and an idle switch no longer flashes it. - let pos = settings::get_settings(overlay_window.app_handle()).overlay_position; - with_gtk_window(&overlay_window, move |gtk_window| { - use gtk::prelude::WidgetExt; - if gtk_window.is_mapped() { - gtk_remap_layer_surface(gtk_window, pos); - } else { - set_layer_shell_anchors(gtk_window, pos); - } - }); + let position = settings::get_settings(app_handle).overlay_position; + match overlay_window.gtk_window() { + Ok(gtk_window) => configure_layer_shell_position(>k_window, position), + Err(error) => log::error!("Failed to access GTK overlay window: {error}"), + } return; } - #[cfg(target_os = "linux")] - if !LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { - // Non-layer-shell fallback on Linux: anchors don't apply, just move. - position_overlay_window(&overlay_window, app_handle); - } - #[cfg(target_os = "windows")] { let state = if WINDOWS_OVERLAY_IS_STREAMING.load(Ordering::Relaxed) { @@ -751,28 +653,31 @@ fn update_overlay_position_on_main(app_handle: &AppHandle) { } } - #[cfg(not(any(target_os = "windows", target_os = "linux")))] - position_overlay_window(&overlay_window, app_handle); + #[cfg(not(target_os = "windows"))] + { + // Use the window's current size so centering stays correct whether the + // overlay is in compact or streaming layout. + let (width, height) = current_overlay_logical_size(&overlay_window) + .unwrap_or((OVERLAY_WIDTH, OVERLAY_HEIGHT)); + if let Some((x, y)) = calculate_overlay_position(app_handle, width, height) { + let _ = overlay_window + .set_position(tauri::Position::Logical(tauri::LogicalPosition { x, y })); + } + } } } /// Hides the recording overlay window with fade-out animation pub fn hide_recording_overlay(app_handle: &AppHandle) { - #[cfg(target_os = "linux")] - use gtk::prelude::WidgetExt; + // Always hide the overlay regardless of settings - if setting was changed while recording, + // we still want to hide it properly if let Some(overlay_window) = app_handle.get_webview_window("recording_overlay") { + // Emit event to trigger fade-out animation let _ = overlay_window.emit("hide-overlay", ()); + // Hide the window after a short delay to allow animation to complete let window_clone = overlay_window.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(300)); - #[cfg(target_os = "linux")] - if LAYER_SHELL_ACTIVE.load(Ordering::SeqCst) { - with_gtk_window(&window_clone, |gtk_window| gtk_window.hide()); - // Keep Tauri's visibility state in sync so the next - // `overlay_window.show()` (which re-flips the flag) is honored. - let _ = window_clone.hide(); - } - #[cfg(not(target_os = "linux"))] let _ = window_clone.hide(); }); } diff --git a/src/bindings.ts b/src/bindings.ts index 3d310fcaef..e7ef242efa 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -857,11 +857,10 @@ async updateRecordingRetentionPeriod(period: string) : Promise> { try { From ba4a90dcd766093e86e1113443be7eef126e4ca6 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sun, 9 Aug 2026 17:04:19 +0800 Subject: [PATCH 11/11] fix: defer Wayland monitor selection --- src-tauri/src/overlay.rs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 07498213ed..655c5e2eb8 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -97,7 +97,6 @@ fn configure_layer_shell_position(gtk_window: >k::ApplicationWindow, position: #[cfg(target_os = "linux")] fn configure_layer_shell_surface( gtk_window: >k::ApplicationWindow, - app_handle: &AppHandle, position: OverlayPosition, width: f64, height: f64, @@ -106,17 +105,6 @@ fn configure_layer_shell_surface( configure_layer_shell_position(gtk_window, position); - // Prefer the output containing the cursor when global cursor coordinates - // are available. On restricted Wayland compositors Enigo may not expose - // them; leaving the monitor unset lets the compositor choose instead. - if let Some((x, y)) = input::get_cursor_position(app_handle) { - if let Some(monitor) = - gtk::gdk::Display::default().and_then(|display| display.monitor_at_point(x, y)) - { - gtk_window.set_monitor(&monitor); - } - } - gtk_window.set_size_request( width.round().max(1.0) as i32, height.round().max(1.0) as i32, @@ -160,13 +148,7 @@ fn init_gtk_layer_shell(overlay_window: &tauri::webview::WebviewWindow) -> bool gtk_window.set_exclusive_zone(0); let overlay_position = settings::get_settings(overlay_window.app_handle()).overlay_position; - configure_layer_shell_surface( - >k_window, - overlay_window.app_handle(), - overlay_position, - OVERLAY_WIDTH, - OVERLAY_HEIGHT, - ); + configure_layer_shell_surface(>k_window, overlay_position, OVERLAY_WIDTH, OVERLAY_HEIGHT); let initialized = gtk_window.is_layer_window(); LAYER_SHELL_ACTIVE.store(initialized, Ordering::SeqCst); @@ -527,7 +509,7 @@ fn show_overlay_state_on_main(app_handle: &AppHandle, state: &str) { let position = settings::get_settings(app_handle).overlay_position; match overlay_window.gtk_window() { Ok(gtk_window) => { - configure_layer_shell_surface(>k_window, app_handle, position, width, height) + configure_layer_shell_surface(>k_window, position, width, height) } Err(error) => log::error!("Failed to access GTK overlay window: {error}"), }