mirror of
https://github.com/gustavosett/Windows-11-Clipboard-History-For-Linux
synced 2026-04-25 17:15:35 +02:00
chore: disables transparency for Nvidia and App Image temporarily (#177)
* docs: update README to clarify window transparency handling * feat: implement rendering environment detection * chore: optimize rendering environment hook * fix: improve NVIDIA detection
This commit is contained in:
23
README.md
23
README.md
@@ -201,6 +201,8 @@ sudo setfacl -m u:$USER:rw /dev/uinput
|
||||
|
||||
> **Note:** You may need to log out and back in for the permanent udev rules to take full effect.
|
||||
|
||||
> **Note:** Window transparency is automatically disabled when running as an AppImage to prevent rendering artefacts. The app will use a solid opaque background instead. See [Troubleshooting → Transparency](#transparency--opacity-rendering-issues-nvidia-or-appimage) for details.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -411,6 +413,27 @@ Config auto-reloads.
|
||||
4. **X11:** Ensure `xclip` is installed
|
||||
5. The app simulates `Ctrl+V` — ensure the target app accepts this shortcut
|
||||
|
||||
### Transparency / opacity rendering issues (NVIDIA or AppImage)
|
||||
|
||||
Users running **NVIDIA proprietary drivers** or launching the app via **AppImage** may experience visual glitches with window transparency (black background, flickering, or garbled content). The app detects these environments automatically and:
|
||||
|
||||
1. Sets `WEBKIT_DISABLE_DMABUF_RENDERER=1` to work around WebKit DMA-BUF bugs.
|
||||
2. Forces window opacity to **100 %** (fully opaque) and removes rounded window corners.
|
||||
3. Disables the transparency sliders in **Settings → Window Transparency** with an explanatory message.
|
||||
|
||||
If detection fails or you want to force this behaviour manually, set the environment variable before launching:
|
||||
|
||||
```bash
|
||||
# Force NVIDIA workaround
|
||||
IS_NVIDIA=1 win11-clipboard-history
|
||||
|
||||
# Force AppImage workaround
|
||||
IS_APPIMAGE=1 win11-clipboard-history
|
||||
|
||||
# Or disable the DMA-BUF renderer directly
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER=1 win11-clipboard-history
|
||||
```
|
||||
|
||||
### Window appears on the wrong monitor
|
||||
The app uses smart cursor tracking. If it appears incorrectly, try moving your mouse to the center of the desired screen and pressing the hotkey again.
|
||||
|
||||
|
||||
@@ -89,6 +89,53 @@ sanitize_xdg_data_dirs() {
|
||||
}
|
||||
sanitize_xdg_data_dirs
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NVIDIA GPU detection
|
||||
# ---------------------------------------------------------------------------
|
||||
# NVIDIA GPUs with proprietary drivers have known issues with WebKit's
|
||||
# DMA-BUF renderer causing opacity/transparency rendering artifacts.
|
||||
# Detect via lspci or the nvidia kernel module presence.
|
||||
detect_nvidia() {
|
||||
# Check if user has already forced the flag
|
||||
if [[ -n "${IS_NVIDIA:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Method 1: Check for loaded nvidia kernel module
|
||||
if lsmod 2>/dev/null | grep -qi '^nvidia'; then
|
||||
export IS_NVIDIA=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Method 2: Check lspci for NVIDIA VGA controller
|
||||
if command -v lspci &>/dev/null && lspci 2>/dev/null | grep -qi 'vga.*nvidia'; then
|
||||
export IS_NVIDIA=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
detect_nvidia
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AppImage detection
|
||||
# ---------------------------------------------------------------------------
|
||||
# AppImage bundles may ship incompatible Mesa/GL libraries that conflict
|
||||
# with the host GPU driver, causing similar opacity rendering issues.
|
||||
if [[ -n "${APPIMAGE:-}" ]]; then
|
||||
export IS_APPIMAGE=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WebKit DMA-BUF workaround for NVIDIA / AppImage
|
||||
# ---------------------------------------------------------------------------
|
||||
# When running on NVIDIA or inside an AppImage, disable the DMA-BUF
|
||||
# renderer in WebKitGTK to prevent opacity/transparency glitches.
|
||||
if [[ "${IS_NVIDIA:-}" == "1" || "${IS_APPIMAGE:-}" == "1" ]]; then
|
||||
echo "Info: Disabling WebKit DMA-BUF renderer due to NVIDIA GPU or AppImage environment."
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Display & rendering defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod focus_manager;
|
||||
pub mod gif_manager;
|
||||
pub mod input_simulator;
|
||||
pub mod permission_checker;
|
||||
pub mod rendering_env;
|
||||
pub mod session;
|
||||
pub mod shortcut_conflict_detector;
|
||||
pub mod shortcut_setup;
|
||||
@@ -28,6 +29,7 @@ pub use permission_checker::{
|
||||
check_permissions, fix_permissions_now, is_first_run, mark_first_run_complete, reset_first_run,
|
||||
PermissionStatus,
|
||||
};
|
||||
pub use rendering_env::{get_rendering_env, get_rendering_environment, RenderingEnv};
|
||||
pub use session::{get_session_type, is_wayland, is_x11, SessionType};
|
||||
pub use shortcut_conflict_detector::{
|
||||
auto_resolve_conflicts, detect_shortcut_conflicts, ConflictDetectionResult, ShortcutConflict,
|
||||
|
||||
@@ -19,6 +19,7 @@ use win11_clipboard_history_lib::focus_manager::x11_robust_activate;
|
||||
use win11_clipboard_history_lib::focus_manager::{restore_focused_window, save_focused_window};
|
||||
use win11_clipboard_history_lib::input_simulator::simulate_paste_keystroke;
|
||||
use win11_clipboard_history_lib::permission_checker;
|
||||
use win11_clipboard_history_lib::rendering_env;
|
||||
use win11_clipboard_history_lib::session::is_wayland;
|
||||
use win11_clipboard_history_lib::shortcut_setup;
|
||||
use win11_clipboard_history_lib::theme_manager::{self, ThemeInfo};
|
||||
@@ -697,6 +698,10 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// MUST run before Tauri / WebKit init – detects NVIDIA & AppImage and
|
||||
// sets WEBKIT_DISABLE_DMABUF_RENDERER=1 when needed.
|
||||
rendering_env::init();
|
||||
|
||||
// Check if --background flag is present (start minimized to tray)
|
||||
let start_in_background = args.iter().any(|arg| arg == "--background");
|
||||
if start_in_background {
|
||||
@@ -1002,6 +1007,7 @@ fn main() {
|
||||
autostart_manager::autostart_disable,
|
||||
autostart_manager::autostart_is_enabled,
|
||||
autostart_manager::autostart_migrate,
|
||||
rendering_env::get_rendering_environment,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
158
src-tauri/src/rendering_env.rs
Normal file
158
src-tauri/src/rendering_env.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
//! Rendering Environment Detection Module
|
||||
//!
|
||||
//! Centralised detection of environments where transparency and rounded corners
|
||||
//! must be disabled to avoid opacity rendering glitches (NVIDIA GPUs, AppImage builds).
|
||||
//!
|
||||
//! Detection is done **programmatically** so it works even when the wrapper
|
||||
//! script is not in the execution path (e.g. AppImage launches the binary
|
||||
//! directly). The wrapper may *also* set `IS_NVIDIA` / `IS_APPIMAGE` — those
|
||||
//! are respected as overrides.
|
||||
//!
|
||||
//! **IMPORTANT**: [`init()`] must be called very early in `main()`, *before*
|
||||
//! any Tauri / WebKit initialisation, because it sets
|
||||
//! `WEBKIT_DISABLE_DMABUF_RENDERER=1` when needed.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Immutable snapshot of the rendering environment, computed once at startup.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RenderingEnv {
|
||||
/// `true` when an NVIDIA GPU is detected.
|
||||
pub is_nvidia: bool,
|
||||
/// `true` when the app is running from an AppImage.
|
||||
pub is_appimage: bool,
|
||||
/// `true` when **either** flag is set – the frontend uses this as a single
|
||||
/// gate to disable transparency & rounded corners.
|
||||
pub transparency_disabled: bool,
|
||||
/// Human-readable reason string shown in the Settings UI.
|
||||
/// Empty when transparency is supported.
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// Singleton – computed once by [`init()`] and read thereafter.
|
||||
static RENDERING_ENV: OnceLock<RenderingEnv> = OnceLock::new();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detection helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Detect NVIDIA GPU presence.
|
||||
///
|
||||
/// Order of checks:
|
||||
/// 1. `IS_NVIDIA=1` env var (set by wrapper or user).
|
||||
/// 2. `/proc/modules` contains a loaded `nvidia` kernel module.
|
||||
/// 3. `lspci` output mentions an NVIDIA VGA controller.
|
||||
fn detect_nvidia() -> bool {
|
||||
// 1. Explicit env override
|
||||
if std::env::var("IS_NVIDIA")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Check loaded kernel modules (fast, no subprocess)
|
||||
if let Ok(modules) = std::fs::read_to_string("/proc/modules") {
|
||||
// Each line starts with the module name followed by a space.
|
||||
// The NVIDIA driver suite loads multiple modules: nvidia, nvidia_drm,
|
||||
// nvidia_modeset, nvidia_uvm — match any of them.
|
||||
for line in modules.lines() {
|
||||
if let Some(name) = line.split_whitespace().next() {
|
||||
if name.to_ascii_lowercase().starts_with("nvidia") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to lspci
|
||||
if let Ok(output) = Command::new("lspci").output() {
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if stdout.lines().any(|l| {
|
||||
l.to_ascii_lowercase().contains("vga") && l.to_ascii_lowercase().contains("nvidia")
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Detect whether we are running inside an AppImage.
|
||||
///
|
||||
/// The AppImage runtime always sets the `APPIMAGE` env var pointing to the
|
||||
/// `.AppImage` file path. We also accept the explicit `IS_APPIMAGE=1`
|
||||
/// override that the wrapper may set.
|
||||
fn detect_appimage() -> bool {
|
||||
if std::env::var("IS_APPIMAGE")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// The standard AppImage runtime sets $APPIMAGE
|
||||
std::env::var("APPIMAGE")
|
||||
.map(|v| !v.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **Must be called at the very start of `main()`** before Tauri / WebKit init.
|
||||
///
|
||||
/// Performs detection, caches the result, sets `WEBKIT_DISABLE_DMABUF_RENDERER`
|
||||
/// if needed, and logs the outcome.
|
||||
pub fn init() {
|
||||
let env = RENDERING_ENV.get_or_init(|| {
|
||||
let is_nvidia = detect_nvidia();
|
||||
let is_appimage = detect_appimage();
|
||||
let transparency_disabled = is_nvidia || is_appimage;
|
||||
|
||||
let reason = if is_nvidia && is_appimage {
|
||||
"Transparency is not supported on NVIDIA GPUs running via AppImage.".to_string()
|
||||
} else if is_nvidia {
|
||||
"Transparency is not supported on NVIDIA GPUs due to rendering issues.".to_string()
|
||||
} else if is_appimage {
|
||||
"Transparency is not supported when running as an AppImage.".to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
RenderingEnv {
|
||||
is_nvidia,
|
||||
is_appimage,
|
||||
transparency_disabled,
|
||||
reason,
|
||||
}
|
||||
});
|
||||
|
||||
// Set the WebKit env var *before* any WebView is created.
|
||||
if env.transparency_disabled {
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
println!(
|
||||
"[RenderingEnv] WEBKIT_DISABLE_DMABUF_RENDERER=1 (NVIDIA={}, AppImage={})",
|
||||
env.is_nvidia, env.is_appimage
|
||||
);
|
||||
} else {
|
||||
println!("[RenderingEnv] Transparency enabled (no NVIDIA/AppImage detected)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the cached rendering environment (panics if [`init()`] was not called).
|
||||
pub fn get_rendering_env() -> &'static RenderingEnv {
|
||||
RENDERING_ENV
|
||||
.get()
|
||||
.expect("rendering_env::init() must be called before get_rendering_env()")
|
||||
}
|
||||
|
||||
/// Tauri command – returns the rendering environment to the frontend.
|
||||
#[tauri::command]
|
||||
pub fn get_rendering_environment() -> RenderingEnv {
|
||||
get_rendering_env().clone()
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { KaomojiPicker } from './components/KaomojiPicker'
|
||||
import { SymbolPicker } from './components/SymbolPicker'
|
||||
import { calculateSecondaryOpacity, calculateTertiaryOpacity } from './utils/themeUtils'
|
||||
import { useSystemThemePreference } from './utils/systemTheme'
|
||||
import { useRenderingEnv } from './hooks/useRenderingEnv'
|
||||
import type { ActiveTab, UserSettings } from './types/clipboard'
|
||||
import { ClipboardTab } from './components/ClipboardTab'
|
||||
|
||||
@@ -93,8 +94,18 @@ function ClipboardApp() {
|
||||
const [settings, setSettings] = useState<UserSettings>(DEFAULT_SETTINGS)
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false)
|
||||
|
||||
const renderingEnv = useRenderingEnv()
|
||||
const isDark = useThemeMode(settings.theme_mode)
|
||||
const opacity = isDark ? settings.dark_background_opacity : settings.light_background_opacity
|
||||
|
||||
// When transparency is disabled (NVIDIA / AppImage) force opacity to fully opaque
|
||||
const effectiveDarkOpacity = renderingEnv.transparency_disabled
|
||||
? 1
|
||||
: settings.dark_background_opacity
|
||||
const effectiveLightOpacity = renderingEnv.transparency_disabled
|
||||
? 1
|
||||
: settings.light_background_opacity
|
||||
|
||||
const opacity = isDark ? effectiveDarkOpacity : effectiveLightOpacity
|
||||
const secondaryOpacity = calculateSecondaryOpacity(opacity)
|
||||
const tertiaryOpacity = calculateTertiaryOpacity(opacity)
|
||||
|
||||
@@ -144,6 +155,21 @@ function ClipboardApp() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Re-apply CSS opacity variables whenever renderingEnv or settings change
|
||||
useEffect(() => {
|
||||
if (renderingEnv.transparency_disabled) {
|
||||
// Force fully opaque CSS variables
|
||||
const opaque: UserSettings = {
|
||||
...settings,
|
||||
dark_background_opacity: 1,
|
||||
light_background_opacity: 1,
|
||||
}
|
||||
applyBackgroundOpacity(opaque)
|
||||
} else {
|
||||
applyBackgroundOpacity(settings)
|
||||
}
|
||||
}, [renderingEnv.transparency_disabled, settings])
|
||||
|
||||
// Apply theme class when isDark changes
|
||||
useEffect(() => {
|
||||
applyThemeClass(isDark)
|
||||
@@ -261,7 +287,8 @@ function ClipboardApp() {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'h-screen w-screen overflow-hidden flex flex-col rounded-win11-lg select-none',
|
||||
'h-screen w-screen overflow-hidden flex flex-col select-none',
|
||||
renderingEnv.transparency_disabled ? 'rounded-none' : 'rounded-win11-lg',
|
||||
isDark ? 'glass-effect' : 'glass-effect-light',
|
||||
isDark ? 'bg-win11-acrylic-bg' : 'bg-win11Light-acrylic-bg',
|
||||
isDark ? 'text-win11-text-primary' : 'text-win11Light-text-primary'
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { UserSettings, CustomKaomoji, BooleanSettingKey } from './types/cli
|
||||
import { FeaturesSection } from './components/FeaturesSection'
|
||||
import { Switch } from './components/Switch'
|
||||
import { useSystemThemePreference } from './utils/systemTheme'
|
||||
import { useRenderingEnv } from './hooks/useRenderingEnv'
|
||||
|
||||
const MIN_HISTORY_SIZE = 1
|
||||
const MAX_HISTORY_SIZE = 100_000
|
||||
@@ -125,6 +126,9 @@ function SettingsApp() {
|
||||
// Custom Kaomoji State
|
||||
const [newKaomoji, setNewKaomoji] = useState('')
|
||||
|
||||
// Rendering environment (NVIDIA / AppImage detection)
|
||||
const renderingEnv = useRenderingEnv()
|
||||
|
||||
// Apply theme to settings window itself
|
||||
const isDark = useThemeMode(settings.theme_mode)
|
||||
|
||||
@@ -543,7 +547,8 @@ function SettingsApp() {
|
||||
<section
|
||||
className={clsx(
|
||||
'rounded-xl border shadow-sm overflow-hidden',
|
||||
isDark ? 'bg-win11-bg-secondary border-white/5' : 'bg-white border-gray-200/60'
|
||||
isDark ? 'bg-win11-bg-secondary border-white/5' : 'bg-white border-gray-200/60',
|
||||
renderingEnv.transparency_disabled && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-inherit">
|
||||
@@ -553,6 +558,43 @@ function SettingsApp() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{renderingEnv.transparency_disabled && (
|
||||
<div
|
||||
className={clsx(
|
||||
'mx-6 mt-6 p-3 rounded-lg flex items-start gap-3 text-sm',
|
||||
isDark ? 'bg-yellow-500/10 text-yellow-300' : 'bg-yellow-50 text-yellow-800'
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="flex-shrink-0 mt-0.5"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-medium text-xs">{renderingEnv.reason}</p>
|
||||
<p
|
||||
className={clsx(
|
||||
'text-[11px] mt-1',
|
||||
isDark ? 'text-yellow-400/70' : 'text-yellow-700'
|
||||
)}
|
||||
>
|
||||
Transparency and rounded window corners have been automatically disabled to
|
||||
prevent rendering artefacts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Dark Mode Slider */}
|
||||
<div className="space-y-4">
|
||||
@@ -566,7 +608,9 @@ function SettingsApp() {
|
||||
isDark ? 'bg-black/20' : 'bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{Math.round(settings.dark_background_opacity * 100)}%
|
||||
{renderingEnv.transparency_disabled
|
||||
? '100%'
|
||||
: `${Math.round(settings.dark_background_opacity * 100)}%`}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
@@ -575,11 +619,15 @@ function SettingsApp() {
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={settings.dark_background_opacity}
|
||||
value={renderingEnv.transparency_disabled ? 1 : settings.dark_background_opacity}
|
||||
onChange={(e) => handleDarkOpacityChange(Number.parseFloat(e.target.value))}
|
||||
onMouseUp={commitOpacityChange}
|
||||
onTouchEnd={commitOpacityChange}
|
||||
className="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-win11-bg-accent"
|
||||
disabled={renderingEnv.transparency_disabled}
|
||||
className={clsx(
|
||||
'w-full h-1.5 bg-gray-200 rounded-lg appearance-none dark:bg-gray-700 accent-win11-bg-accent',
|
||||
renderingEnv.transparency_disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -595,7 +643,9 @@ function SettingsApp() {
|
||||
isDark ? 'bg-black/20' : 'bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{Math.round(settings.light_background_opacity * 100)}%
|
||||
{renderingEnv.transparency_disabled
|
||||
? '100%'
|
||||
: `${Math.round(settings.light_background_opacity * 100)}%`}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
@@ -604,11 +654,15 @@ function SettingsApp() {
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={settings.light_background_opacity}
|
||||
value={renderingEnv.transparency_disabled ? 1 : settings.light_background_opacity}
|
||||
onChange={(e) => handleLightOpacityChange(Number.parseFloat(e.target.value))}
|
||||
onMouseUp={commitOpacityChange}
|
||||
onTouchEnd={commitOpacityChange}
|
||||
className="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-win11-bg-accent"
|
||||
disabled={renderingEnv.transparency_disabled}
|
||||
className={clsx(
|
||||
'w-full h-1.5 bg-gray-200 rounded-lg appearance-none dark:bg-gray-700 accent-win11-bg-accent',
|
||||
renderingEnv.transparency_disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
74
src/hooks/useRenderingEnv.ts
Normal file
74
src/hooks/useRenderingEnv.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { RenderingEnv } from '../types/clipboard'
|
||||
|
||||
const DEFAULT_RENDERING_ENV: RenderingEnv = {
|
||||
is_nvidia: false,
|
||||
is_appimage: false,
|
||||
transparency_disabled: false,
|
||||
reason: '',
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached rendering environment shared across all hook consumers.
|
||||
* This avoids repeated IPC calls to `get_rendering_environment`.
|
||||
*/
|
||||
let cachedEnv: RenderingEnv | null = null
|
||||
|
||||
/**
|
||||
* Tracks an in-flight request so concurrent hook calls can share the same
|
||||
* promise instead of issuing multiple IPC calls.
|
||||
*/
|
||||
let pendingEnvPromise: Promise<RenderingEnv> | null = null
|
||||
|
||||
/**
|
||||
* Queries the backend once for the rendering environment (NVIDIA / AppImage
|
||||
* detection) and caches the result process-wide.
|
||||
*
|
||||
* Multiple components can safely call this hook — only one IPC call will be made.
|
||||
*
|
||||
* When `transparency_disabled` is `true` the caller should:
|
||||
* - Force opacity to 1 (fully opaque)
|
||||
* - Remove rounded outer corners (use `rounded-none`)
|
||||
* - Disable the transparency sliders in Settings
|
||||
*/
|
||||
export function useRenderingEnv() {
|
||||
const [env, setEnv] = useState<RenderingEnv>(cachedEnv ?? DEFAULT_RENDERING_ENV)
|
||||
|
||||
useEffect(() => {
|
||||
// If we already have a cached env, state is already initialized - skip IPC
|
||||
if (cachedEnv) {
|
||||
return
|
||||
}
|
||||
|
||||
// Reuse an in-flight request if one exists
|
||||
if (!pendingEnvPromise) {
|
||||
pendingEnvPromise = invoke<RenderingEnv>('get_rendering_environment')
|
||||
.then((result) => {
|
||||
cachedEnv = result
|
||||
return result
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to query rendering environment:', err)
|
||||
// On error, keep cachedEnv as-is (likely null) so we can retry
|
||||
// on next mount if desired
|
||||
throw err
|
||||
})
|
||||
.finally(() => {
|
||||
pendingEnvPromise = null
|
||||
})
|
||||
}
|
||||
|
||||
pendingEnvPromise
|
||||
.then((result) => {
|
||||
// Guard against cases where the component unmounts; React ignores
|
||||
// state updates on unmounted components so this is safe
|
||||
setEnv(result)
|
||||
})
|
||||
.catch(() => {
|
||||
// Error already logged above; keep default env in state
|
||||
})
|
||||
}, [])
|
||||
|
||||
return env
|
||||
}
|
||||
@@ -133,6 +133,23 @@ body {
|
||||
0 8px 32px rgba(0, 0, 0, 0.15),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/*
|
||||
* Solid-background fallback for NVIDIA / AppImage environments where
|
||||
* transparency causes rendering artefacts. Applied automatically when
|
||||
* the rendering-env hook forces opacity CSS vars to 1.0.
|
||||
*/
|
||||
.rounded-none .glass-effect,
|
||||
.glass-effect.no-transparency {
|
||||
background: rgb(40, 40, 40);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.rounded-none .glass-effect-light,
|
||||
.glass-effect-light.no-transparency {
|
||||
background: rgb(255, 255, 255);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
|
||||
@@ -81,3 +81,15 @@ export interface UserSettings {
|
||||
export type BooleanSettingKey = {
|
||||
[K in keyof UserSettings]: UserSettings[K] extends boolean ? K : never
|
||||
}[keyof UserSettings]
|
||||
|
||||
/** Rendering environment flags from the backend (NVIDIA / AppImage detection) */
|
||||
export interface RenderingEnv {
|
||||
/** true when an NVIDIA GPU is detected */
|
||||
is_nvidia: boolean
|
||||
/** true when running from an AppImage */
|
||||
is_appimage: boolean
|
||||
/** true when transparency & rounded corners must be disabled */
|
||||
transparency_disabled: boolean
|
||||
/** Human-readable reason shown in Settings UI */
|
||||
reason: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user