feat(ui): enable touchpad back/forward navigation gestures

Wire native WebKit/WebView2 swipe-to-navigate settings (issue #935) with
a Settings toggle defaulting on, applied to main and mini webviews.
This commit is contained in:
cucadmuh
2026-06-20 00:33:24 +03:00
parent c037ab459a
commit e0d1af70a2
23 changed files with 186 additions and 8 deletions
+25
View File
@@ -3496,6 +3496,16 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-javascript-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
dependencies = [
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
@@ -3520,6 +3530,17 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "objc2-security"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
dependencies = [
"bitflags 2.11.1",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.2"
@@ -3563,6 +3584,8 @@ dependencies = [
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-javascript-core",
"objc2-security",
]
[[package]]
@@ -4127,6 +4150,7 @@ dependencies = [
"lofty",
"mach2",
"md5",
"objc2-web-kit",
"psysonic-analysis",
"psysonic-audio",
"psysonic-core",
@@ -4160,6 +4184,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-nvidia-quirk",
"webp",
"webview2-com",
"windows 0.62.2",
"zbus 5.16.0",
"zip 8.6.0",
+2
View File
@@ -83,6 +83,7 @@ libc = "0.2"
[target.'cfg(target_os = "macos")'.dependencies]
mach2 = "0.5"
objc2-web-kit = "0.3"
[target.'cfg(target_os = "linux")'.dependencies]
zbus = { version = "5.16", default-features = false, features = ["blocking-api", "async-io"] }
@@ -91,6 +92,7 @@ webkit2gtk = { version = "2.0", default-features = false, features = ["v2_40"] }
webkit2gtk-nvidia-quirk = "1.3"
[target.'cfg(windows)'.dependencies]
webview2-com = "0.38"
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_Graphics",
+1
View File
@@ -689,6 +689,7 @@ pub fn run() {
cli_publish_server_list,
cli_publish_search_results,
set_window_decorations,
set_back_forward_navigation_gestures,
set_linux_webkit_smooth_scrolling,
linux_wayland_gpu_font_tuning_active,
linux_wayland_text_render_settings_available,
+2 -1
View File
@@ -22,7 +22,8 @@ pub(crate) use core::{
pub(crate) use perf::performance_cpu_snapshot;
pub(crate) use platform::{
linux_wayland_gpu_font_tuning_active, linux_wayland_text_render_settings_available,
set_linux_wayland_text_render_profile, set_linux_webkit_smooth_scrolling, set_window_decorations,
set_back_forward_navigation_gestures, set_linux_wayland_text_render_profile,
set_linux_webkit_smooth_scrolling, set_window_decorations,
theme_animation_risk,
};
#[cfg(target_os = "linux")]
@@ -208,6 +208,85 @@ pub(crate) fn linux_webkit_apply_smooth_scrolling(win: &tauri::WebviewWindow, en
.map_err(|e| e.to_string())
}
/// WebKitGTK / WKWebView / WebView2: two-finger horizontal swipe for history back/forward.
#[cfg(target_os = "linux")]
pub(crate) fn webview_apply_back_forward_navigation_gestures(
win: &tauri::WebviewWindow,
enabled: bool,
) -> Result<(), String> {
win.with_webview(move |platform| {
use webkit2gtk::{SettingsExt, WebViewExt};
if let Some(settings) = platform.inner().settings() {
settings.set_enable_back_forward_navigation_gestures(enabled);
}
})
.map_err(|e| e.to_string())
}
#[cfg(target_os = "macos")]
pub(crate) fn webview_apply_back_forward_navigation_gestures(
win: &tauri::WebviewWindow,
enabled: bool,
) -> Result<(), String> {
win.with_webview(move |platform| {
use objc2_web_kit::WKWebView;
let ptr = platform.inner();
if ptr.is_null() {
return;
}
unsafe {
let webview = &*(ptr as *const WKWebView);
webview.setAllowsBackForwardNavigationGestures(enabled);
}
})
.map_err(|e| e.to_string())
}
#[cfg(windows)]
pub(crate) fn webview_apply_back_forward_navigation_gestures(
win: &tauri::WebviewWindow,
enabled: bool,
) -> Result<(), String> {
use webview2_com::Microsoft::Web::WebView2::Win32::*;
use windows::core::Interface;
win.with_webview(move |platform| {
let controller = platform.controller();
unsafe {
if let Ok(webview) = controller.CoreWebView2() {
if let Ok(settings) = webview.Settings() {
if let Ok(settings6) = settings.cast::<ICoreWebView2Settings6>() {
let _ = settings6.SetIsSwipeNavigationEnabled(enabled);
}
}
}
}
})
.map_err(|e| e.to_string())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
pub(crate) fn webview_apply_back_forward_navigation_gestures(
_win: &tauri::WebviewWindow,
_enabled: bool,
) -> Result<(), String> {
Ok(())
}
/// Called from the frontend settings toggle; applies to every open webview window.
#[tauri::command]
pub(crate) fn set_back_forward_navigation_gestures(
enabled: bool,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
use tauri::Manager;
for label in ["main", "mini"] {
if let Some(win) = app_handle.get_webview_window(label) {
webview_apply_back_forward_navigation_gestures(&win, enabled)?;
}
}
Ok(())
}
/// Called from the frontend settings toggle (Linux); no-op on other platforms.
#[tauri::command]
pub(crate) fn set_linux_webkit_smooth_scrolling(enabled: bool, app_handle: tauri::AppHandle) -> Result<(), String> {
+1
View File
@@ -46,6 +46,7 @@ beforeEach(() => {
onInvoke('audio_get_state', () => ({ playing: false }));
onInvoke('set_window_always_on_top', () => undefined);
onInvoke('set_linux_webkit_smooth_scrolling', () => undefined);
onInvoke('set_back_forward_navigation_gestures', () => undefined);
onInvoke('discord_update_presence', () => undefined);
});
+7
View File
@@ -99,6 +99,13 @@ export function SystemTab() {
checked={auth.minimizeToTray}
onChange={auth.setMinimizeToTray}
/>
<div className="settings-section-divider" />
<SettingsToggle
label={t('settings.touchpadBackForwardGestures')}
desc={t('settings.touchpadBackForwardGesturesDesc')}
checked={auth.touchpadBackForwardGestures}
onChange={auth.setTouchpadBackForwardGestures}
/>
</SettingsGroup>
{IS_LINUX && (
+1 -1
View File
@@ -71,7 +71,7 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'input', titleKey: 'settings.inputKeybindingsTitle', keywords: 'keybindings shortcuts hotkeys keyboard' },
{ tab: 'input', titleKey: 'settings.globalShortcutsTitle', keywords: 'global shortcuts hotkeys system-wide media keys' },
{ tab: 'system', titleKey: 'settings.language', keywords: 'language locale translation i18n' },
{ tab: 'system', titleKey: 'settings.behavior', keywords: 'behavior tray minimize close start smooth scroll linux' },
{ tab: 'system', titleKey: 'settings.behavior', keywords: 'behavior tray minimize close start smooth scroll linux swipe back forward touchpad gesture navigation' },
{ tab: 'system', titleKey: 'settings.backupTitle', keywords: 'backup export import settings restore' },
{ tab: 'system', titleKey: 'settings.loggingTitle', keywords: 'log logs diagnostic debug verbose' },
{ tab: 'system', titleKey: 'settings.aboutTitle', keywords: 'about version update changelog release notes' },
+10 -6
View File
@@ -16,15 +16,19 @@ import {
* actually brings the window to the foreground. */
export function useMiniWindowSetup(alwaysOnTop: boolean, initialQueueOpen: boolean) {
useEffect(() => {
if (!IS_LINUX) return;
const apply = () => {
invoke('set_linux_webkit_smooth_scrolling', {
enabled: useAuthStore.getState().linuxWebkitKineticScroll,
const applyWebviewPrefs = () => {
invoke('set_back_forward_navigation_gestures', {
enabled: useAuthStore.getState().touchpadBackForwardGestures,
}).catch(() => {});
if (IS_LINUX) {
invoke('set_linux_webkit_smooth_scrolling', {
enabled: useAuthStore.getState().linuxWebkitKineticScroll,
}).catch(() => {});
}
};
apply();
applyWebviewPrefs();
return useAuthStore.persist.onFinishHydration(() => {
apply();
applyWebviewPrefs();
});
}, []);
+19
View File
@@ -15,6 +15,7 @@ export function usePlatformShellSetup(): { isTilingWm: boolean } {
const [isTilingWm, setIsTilingWm] = useState(false);
const [waylandTextUi, setWaylandTextUi] = useState(false);
const useCustomTitlebar = useAuthStore(s => s.useCustomTitlebar);
const touchpadBackForwardGestures = useAuthStore(s => s.touchpadBackForwardGestures);
const linuxWebkitKineticScroll = useAuthStore(s => s.linuxWebkitKineticScroll);
const linuxWaylandTextRenderProfile = useAuthStore(s => s.linuxWaylandTextRenderProfile);
const loggingMode = useAuthStore(s => s.loggingMode);
@@ -87,6 +88,24 @@ export function usePlatformShellSetup(): { isTilingWm: boolean } {
invoke('set_window_decorations', { enabled }).catch(() => {});
}, [useCustomTitlebar, isTilingWm]);
useEffect(() => {
invoke('set_back_forward_navigation_gestures', { enabled: touchpadBackForwardGestures }).catch(() => {});
}, [touchpadBackForwardGestures]);
useEffect(() => {
const applyFromStore = () => {
invoke('set_back_forward_navigation_gestures', {
enabled: useAuthStore.getState().touchpadBackForwardGestures,
}).catch(() => {});
};
if (useAuthStore.persist.hasHydrated()) {
applyFromStore();
}
return useAuthStore.persist.onFinishHydration(() => {
applyFromStore();
});
}, []);
useEffect(() => {
if (!IS_LINUX) return;
invoke('set_linux_webkit_smooth_scrolling', { enabled: linuxWebkitKineticScroll }).catch(() => {});
+3
View File
@@ -220,6 +220,9 @@ export const settings = {
showTrayIconDesc: 'Psysonic-Icon im System-Tray / in der Menüleiste anzeigen.',
minimizeToTray: 'Im Tray minimieren',
minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.',
touchpadBackForwardGestures: 'Wischen für Zurück / Vor',
touchpadBackForwardGesturesDesc:
'Zwei-Finger-Wischgeste auf dem Touchpad blättert in der App-Navigation wie im Browser.',
clockFormat: 'Uhrzeitformat',
clockFormatDesc: 'Format für die Queue-ETA und die Vorschau des Sleep-Timers.',
clockFormatAuto: 'Automatisch (Systemsprache)',
+6
View File
@@ -223,6 +223,12 @@ export const settings = {
showTrayIconDesc: 'Display the Psysonic icon in the system notification area / menu bar.',
minimizeToTray: 'Minimize to Tray',
minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.',
touchpadBackForwardGestures: 'Swipe to go back / forward',
touchpadBackForwardGesturesDesc:
'Two-finger horizontal touchpad swipe navigates in-app history, like in a browser.',
touchpadBackForwardGestures: 'Swipe to go back / forward',
touchpadBackForwardGesturesDesc:
'Two-finger horizontal touchpad swipe navigates in-app history, like in a browser.',
clockFormat: 'Clock Format',
clockFormatDesc: 'Wall-clock display used by the queue ETA and the sleep-timer preview.',
clockFormatAuto: 'Auto (system locale)',
+3
View File
@@ -219,6 +219,9 @@ export const settings = {
showTrayIconDesc: 'Muestra el icono de Psysonic en el área de notificación / barra de menú.',
minimizeToTray: 'Minimizar a Bandeja',
minimizeToTrayDesc: 'Al cerrar la ventana, mantener Psysonic ejecutándose en la bandeja del sistema en lugar de salir.',
touchpadBackForwardGestures: 'Deslizar para atrás / adelante',
touchpadBackForwardGesturesDesc:
'Deslizar horizontalmente con dos dedos en el touchpad navega el historial de la app, como en un navegador.',
clockFormat: 'Formato de hora',
clockFormatDesc: 'Formato del reloj utilizado por la ETA de la cola y la vista previa del temporizador de suspensión.',
clockFormatAuto: 'Automático (idioma del sistema)',
+3
View File
@@ -219,6 +219,9 @@ export const settings = {
showTrayIconDesc: 'Affiche l\'icône Psysonic dans la zone de notification / barre des menus.',
minimizeToTray: 'Réduire dans la barre système',
minimizeToTrayDesc: 'Lors de la fermeture, Psysonic continue de fonctionner dans la barre système au lieu de se fermer.',
touchpadBackForwardGestures: 'Balayer pour précédent / suivant',
touchpadBackForwardGesturesDesc:
'Un balayage horizontal à deux doigts sur le pavé tactile parcourt lhistorique de navigation, comme dans un navigateur.',
clockFormat: 'Format de l\'heure',
clockFormatDesc: 'Format de l\'horloge utilisé par l\'ETA de la file d\'attente et l\'aperçu du minuteur de mise en veille.',
clockFormatAuto: 'Automatique (langue du système)',
+3
View File
@@ -218,6 +218,9 @@ export const settings = {
showOrbitTriggerDesc: 'Knappen i toppen for å starte eller bli med i en delt lytteøkt. Skjul den hvis du ikke bruker Orbit — du kan slå den på igjen her.',
minimizeToTray: 'Minimer til oppgavelinjen',
minimizeToTrayDesc: 'Når vinduet lukkes, vil Psysonic bli kjørende i oppgavelinjen fremfor å bli avsluttet.',
touchpadBackForwardGestures: 'Sveip tilbake / frem',
touchpadBackForwardGesturesDesc:
'Horisontalt sveip med to fingre på styreplaten navigerer i appens historikk, som i en nettleser.',
clockFormat: 'Klokkeformat',
clockFormatDesc: 'Klokkevisning brukt av kø-ETA og forhåndsvisningen av hviletimeren.',
clockFormatAuto: 'Automatisk (systemspråk)',
+3
View File
@@ -219,6 +219,9 @@ export const settings = {
showTrayIconDesc: 'Toont het Psysonic-pictogram in het systeemvak / de menubalk.',
minimizeToTray: 'Minimaliseren naar systeemvak',
minimizeToTrayDesc: 'Bij het sluiten van het venster blijft Psysonic actief in het systeemvak in plaats van af te sluiten.',
touchpadBackForwardGestures: 'Veeg terug / vooruit',
touchpadBackForwardGesturesDesc:
'Horizontaal vegen met twee vingers op het touchpad doorloopt de navigatiegeschiedenis, zoals in een browser.',
clockFormat: 'Tijdformaat',
clockFormatDesc: 'Tijdsweergave voor de wachtrij-ETA en het voorbeeld van de slaaptimer.',
clockFormatAuto: 'Automatisch (systeemtaal)',
+3
View File
@@ -222,6 +222,9 @@ export const settings = {
showTrayIconDesc: 'Afișează iconița Psysonic în zona notificărilor de sistem / bara de meniu.',
minimizeToTray: 'Minimizează în Tavă',
minimizeToTrayDesc: 'La închiderea ferestrei, continuă rularea Psysonic în tava de sistem în loc de ieșire',
touchpadBackForwardGestures: 'Glisare înapoi / înainte',
touchpadBackForwardGesturesDesc:
'Glisarea orizontală cu două degete pe touchpad parcurge istoricul de navigare din aplicație, ca într-un browser.',
clockFormat: 'Format Oră',
clockFormatDesc: 'Format al ceasului folosit pentru ETA-ul cozii și previzualizarea cronometrului de somn.',
clockFormatAuto: 'Automat (limba sistemului)',
+6
View File
@@ -224,6 +224,12 @@ export const settings = {
showTrayIconDesc: 'Показывать Psysonic в области уведомлений / строке меню.',
minimizeToTray: 'Сворачивать в трей',
minimizeToTrayDesc: 'При закрытии окна не выходить из приложения, а оставаться в трее.',
touchpadBackForwardGestures: 'Свайп назад / вперёд',
touchpadBackForwardGesturesDesc:
'Горизонтальный свайп двумя пальцами по тачпаду перелистывает историю навигации в приложении, как в браузере.',
touchpadBackForwardGestures: 'Свайп назад / вперёд',
touchpadBackForwardGesturesDesc:
'Горизонтальный свайп двумя пальцами по тачпаду перелистывает историю навигации в приложении, как в браузере.',
clockFormat: 'Формат времени',
clockFormatDesc: 'Формат времени для ETA очереди и предпросмотра таймера сна.',
clockFormatAuto: 'Авто (системный)',
+2
View File
@@ -219,6 +219,8 @@ export const settings = {
showTrayIconDesc: '在系统通知区域 / 菜单栏显示 Psysonic 图标。',
minimizeToTray: '最小化到托盘',
minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。',
touchpadBackForwardGestures: '触控板左右滑动前进/后退',
touchpadBackForwardGesturesDesc: '在触控板上双指水平滑动,可像浏览器一样在应用内历史中前进或后退。',
clockFormat: '时间格式',
clockFormatDesc: '队列预计结束时间和睡眠定时器预览使用的时间格式。',
clockFormatAuto: '自动(跟随系统)',
+1
View File
@@ -61,6 +61,7 @@ describe('trivial pass-through setters', () => {
['setWindowButtonStyle', 'windowButtonStyle', 'flat'],
['setShowMinimizeButton', 'showMinimizeButton', false],
['setPreloadMiniPlayer', 'preloadMiniPlayer', true],
['setTouchpadBackForwardGestures', 'touchpadBackForwardGestures', false],
['setLinuxWebkitKineticScroll', 'linuxWebkitKineticScroll', false],
['setLinuxWaylandTextRenderProfile', 'linuxWaylandTextRenderProfile', 'gpu'],
['setNowPlayingEnabled', 'nowPlayingEnabled', true],
+1
View File
@@ -79,6 +79,7 @@ export const useAuthStore = create<AuthState>()(
windowButtonStyle: 'dots',
showMinimizeButton: true,
preloadMiniPlayer: false,
touchpadBackForwardGestures: true,
linuxWebkitKineticScroll: true,
linuxWaylandTextRenderProfile: 'sharp',
linuxWebkitInputForceRepaint: false,
+3
View File
@@ -172,6 +172,8 @@ export interface AuthState {
/** Pre-build the mini-player webview at app start on Linux/macOS so content is available instantly
* on first open. Ignored on Windows that platform always pre-creates as a hang workaround. */
preloadMiniPlayer: boolean;
/** Two-finger horizontal touchpad swipe for in-app history back/forward (WebKit / WebView2). */
touchpadBackForwardGestures: boolean;
/** Linux WebKitGTK: smooth wheel on when true; off only after explicit opt-out in Settings. */
linuxWebkitKineticScroll: boolean;
/** Linux Wayland + GPU compositing: WebKit text rasterisation profile (live, no restart). */
@@ -375,6 +377,7 @@ export interface AuthState {
setWindowButtonStyle: (v: WindowButtonStyle) => void;
setShowMinimizeButton: (v: boolean) => void;
setPreloadMiniPlayer: (v: boolean) => void;
setTouchpadBackForwardGestures: (v: boolean) => void;
setLinuxWebkitKineticScroll: (v: boolean) => void;
setLinuxWaylandTextRenderProfile: (v: LinuxWaylandTextRenderProfile) => void;
setLinuxWebkitInputForceRepaint: (v: boolean) => void;
+2
View File
@@ -22,6 +22,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
| 'setWindowButtonStyle'
| 'setShowMinimizeButton'
| 'setPreloadMiniPlayer'
| 'setTouchpadBackForwardGestures'
| 'setLinuxWebkitKineticScroll'
| 'setLinuxWaylandTextRenderProfile'
| 'setLinuxWebkitInputForceRepaint'
@@ -46,6 +47,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
setWindowButtonStyle: (v) => set({ windowButtonStyle: v }),
setShowMinimizeButton: (v) => set({ showMinimizeButton: v }),
setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }),
setTouchpadBackForwardGestures: (v) => set({ touchpadBackForwardGestures: v }),
setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }),
setLinuxWaylandTextRenderProfile: (v) => set({ linuxWaylandTextRenderProfile: v }),
setLinuxWebkitInputForceRepaint: (v) => set({ linuxWebkitInputForceRepaint: v }),