diff --git a/src/manager/event_handlers.ts b/src/manager/event_handlers.ts index 0a55a68..670215e 100644 --- a/src/manager/event_handlers.ts +++ b/src/manager/event_handlers.ts @@ -165,7 +165,7 @@ export function onRestacked(): void { export const onSizeChanged = refreshRoundedCorners; -export const onFocusChanged = refreshShadow; +export const onFocusChanged = refreshRoundedCorners; export const onSettingsChanged = refreshAllRoundedCorners; diff --git a/src/manager/event_manager.ts b/src/manager/event_manager.ts index 11788c4..924cda7 100644 --- a/src/manager/event_manager.ts +++ b/src/manager/event_manager.ts @@ -8,9 +8,12 @@ import type Meta from 'gi://Meta'; import type Shell from 'gi://Shell'; import type {RoundedWindowActor} from '../utils/types.js'; +import GLib from 'gi://GLib'; + import {logDebug} from '../utils/log.js'; import {prefs} from '../utils/settings.js'; import * as handlers from './event_handlers.js'; +import {isChromiumWindow} from './utils.js'; /** * The rounded corners effect has to perform some actions when differen events @@ -29,10 +32,54 @@ export function enableEffect() { // Add the effect to all windows when the extension is enabled. const windowActors = global.get_window_actors(); logDebug(`Initial window count: ${windowActors.length}`); + for (const actor of windowActors) { applyEffectTo(actor); } + // When the extension is re-enabled after screen lock/unlock, + // Chromium-based browsers may render stale surfaces. The compositor + // skips repainting GLSL effects for unfocused windows, so briefly + // focusing each affected window forces a repaint and triggers our + // onFocusChanged handler which recomputes shader bounds. + if (windowActors.length > 0) { + deferredRefreshId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { + const focusedWin = global.display.get_focus_window(); + const chromiumWindows = global + .get_window_actors() + .map(a => a.metaWindow) + .filter( + (win): win is Meta.Window => + win != null && + win !== focusedWin && + isChromiumWindow(win), + ); + + if (chromiumWindows.length > 0) { + const timestamp = global.get_current_time(); + for (const [i, win] of chromiumWindows.entries()) { + GLib.timeout_add(GLib.PRIORITY_DEFAULT, i * 100, () => { + win.focus(timestamp); + return GLib.SOURCE_REMOVE; + }); + } + + // Restore focus to the originally focused window. + GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + chromiumWindows.length * 100, + () => { + focusedWin?.focus(global.get_current_time()); + return GLib.SOURCE_REMOVE; + }, + ); + } + + deferredRefreshId = 0; + return GLib.SOURCE_REMOVE; + }); + } + // Add the effect to new windows when they are opened. connect( global.display, @@ -74,6 +121,11 @@ export function enableEffect() { /** Disable the effect for all windows. */ export function disableEffect() { + if (deferredRefreshId) { + GLib.source_remove(deferredRefreshId); + deferredRefreshId = 0; + } + for (const actor of global.get_window_actors()) { removeEffectFrom(actor); } @@ -81,6 +133,7 @@ export function disableEffect() { disconnectAll(); } +let deferredRefreshId = 0; const connections: {object: GObject.Object; id: number}[] = []; /** diff --git a/src/manager/utils.ts b/src/manager/utils.ts index b752395..eee17c3 100644 --- a/src/manager/utils.ts +++ b/src/manager/utils.ts @@ -5,6 +5,7 @@ import type {RoundedCornersEffect} from '../effect/rounded_corners_effect.js'; import type {Bounds, RoundedCornerSettings} from '../utils/types.js'; import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; import Meta from 'gi://Meta'; import St from 'gi://St'; @@ -130,10 +131,11 @@ export function computeBounds( }; // Kitty draws its window decoration by itself, so we need to manually - // clip its shadow and recompute the outer bounds for it. + // clip its shadow and recompute the outer bounds for it. Check wm_class + // first to avoid reading the pref for every non-kitty window. if ( - getPref('tweak-kitty-terminal') && - actor.metaWindow.get_wm_class_instance() === 'kitty' + actor.metaWindow.get_wm_class_instance() === 'kitty' && + getPref('tweak-kitty-terminal') ) { const [x1, y1, x2, y2] = APP_SHADOWS.kitty; const scale = windowScaleFactor(actor.metaWindow); @@ -202,19 +204,17 @@ export function computeShadowActorOffset( export function updateShadowActorStyle( win: Meta.Window, actor: St.Bin, - borderRadius = getPref('global-rounded-corner-settings').borderRadius, + borderRadius?: number, shadow = getPref('focused-shadow'), - padding = getPref('global-rounded-corner-settings').padding, + padding?: RoundedCornerSettings['padding'], ) { - const {left, right, top, bottom} = padding; - - // Increase border_radius when smoothing is on. - // Read global settings once to avoid repeated GSettings deserializations. - let adjustedBorderRadius = borderRadius; const globalCfg = getPref('global-rounded-corner-settings'); - if (globalCfg !== null) { - adjustedBorderRadius *= 1.0 + globalCfg.smoothing; - } + const effectiveBorderRadius = borderRadius ?? globalCfg.borderRadius; + const effectivePadding = padding ?? globalCfg.padding; + + const {left, right, top, bottom} = effectivePadding; + const adjustedBorderRadius = + effectiveBorderRadius * (1.0 + globalCfg.smoothing); // If there are two monitors with different scale factors, the scale of // the window may be different from the scale that has to be applied in @@ -322,6 +322,25 @@ export function shouldEnableEffect( ); } +const CHROMIUM_WM_CLASSES = [ + 'brave-browser', + 'chromium', + 'google-chrome', + 'microsoft-edge', +]; + +/** + * Check whether a window belongs to a Chromium-based browser. These apps + * render stale surfaces for unfocused windows after screen lock/unlock. + * + * @param win - The window to check. + * @returns Whether the window belongs to a Chromium-based browser. + */ +export function isChromiumWindow(win: Meta.Window): boolean { + const wmClass = win.get_wm_class_instance(); + return wmClass != null && CHROMIUM_WM_CLASSES.includes(wmClass); +} + type AppType = 'LibAdwaita' | 'LibHandy' | 'Other'; /** @@ -345,7 +364,21 @@ function getAppType(win: Meta.Window): AppType { return 'Other'; } catch (e) { - logError(e); + // /proc//maps is unreadable for processes owned by another user + // (e.g. flatpak sandboxes), and can disappear if the process exits + // between get_pid() and the read. Both are expected and benign. + const isExpected = + e instanceof GLib.Error && + e.domain === Gio.io_error_quark() && + (e.code === Gio.IOErrorEnum.PERMISSION_DENIED || + e.code === Gio.IOErrorEnum.NOT_FOUND); + if (isExpected) { + logDebug( + `Could not read /proc/${win.get_pid()}/maps: ${(e as GLib.Error).message}`, + ); + } else { + logError(e); + } return 'Other'; } } diff --git a/src/patch/add_shadow_in_overview.ts b/src/patch/add_shadow_in_overview.ts index 40b1460..33486f0 100644 --- a/src/patch/add_shadow_in_overview.ts +++ b/src/patch/add_shadow_in_overview.ts @@ -113,11 +113,15 @@ const OverviewShadowActorClone = GObject.registerClass( return; } + const frameWidth = metaWindow.get_frame_rect().width; + if (frameWidth === 0) { + return; + } + // Scale the shadow by the same scale factor that the window preview // is scaled by. const containerScaleFactor = - windowContainerBox.get_width() / - metaWindow.get_frame_rect().width; + windowContainerBox.get_width() / frameWidth; const paddings = SHADOW_PADDING * containerScaleFactor * diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 37fd50c..660caff 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -56,6 +56,15 @@ export const Schema = { /** The raw GSettings object for direct manipulation. */ export let prefs: Gio.Settings; +/** + * Cache of deserialized pref values. Reading a pref goes through D-Bus and + * Variant unpacking, so hot paths (per-frame shader updates, per-event + * refreshes) read the same keys many times per second. The cache is + * invalidated by connecting to the `changed` signal on init. + */ +const prefCache = new Map(); +let prefCacheChangedId = 0; + /** * Initialize the {@link prefs} object with existing GSettings. * @@ -64,22 +73,38 @@ export let prefs: Gio.Settings; export function initPrefs(gSettings: Gio.Settings) { resetOutdated(gSettings); prefs = gSettings; + prefCache.clear(); + prefCacheChangedId = prefs.connect('changed', (_, key) => { + prefCache.delete(key as SchemaKey); + }); } /** Delete the {@link prefs} object for garbage collection. */ export function uninitPrefs() { + if (prefCacheChangedId) { + prefs.disconnect(prefCacheChangedId); + prefCacheChangedId = 0; + } + prefCache.clear(); (prefs as Gio.Settings | null) = null; } /** * Get a preference from GSettings and convert it from a GLib Variant to a - * JavaScript type. + * JavaScript type. Values are cached and invalidated automatically when the + * underlying setting changes. * * @param key - The key of the preference to get. * @returns The value of the preference. */ export function getPref(key: K): Schema[K] { - return prefs.get_value(key).recursiveUnpack() as Schema[K]; + const cached = prefCache.get(key); + if (cached !== undefined) { + return cached as Schema[K]; + } + const value = prefs.get_value(key).recursiveUnpack() as Schema[K]; + prefCache.set(key, value); + return value; } /**