Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI] Frameless window option (with theme support) #3066

Merged
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a2d96bc
Add frameless window app setting
0xCmdrKeen Sep 19, 2023
90f5c2a
Extract titlebar options from custom theme
0xCmdrKeen Sep 19, 2023
4de4d15
Set titlebar overlay options from theme selector
0xCmdrKeen Sep 22, 2023
9d04555
Make sidebar draggable on frameless windows
0xCmdrKeen Sep 22, 2023
026473b
Fix dead code error
0xCmdrKeen Sep 22, 2023
dc8860f
Add preliminary support for Linux
0xCmdrKeen Sep 22, 2023
aced874
Add proper safety checks
0xCmdrKeen Sep 22, 2023
f5dba27
Remove default titlebar height to let platform decide
0xCmdrKeen Sep 23, 2023
640ae30
Custom WindowControls component for frameless windows
0xCmdrKeen Sep 24, 2023
3a97cf8
Fix broken tests
0xCmdrKeen Sep 24, 2023
1acd01a
Replace isFramless IPC method with injected property
0xCmdrKeen Sep 25, 2023
579fd7d
Prevent overlay controls from covering up content
0xCmdrKeen Sep 25, 2023
787f72c
Deal with macOS overlay controls being on the left
0xCmdrKeen Sep 25, 2023
93a0000
Fix for non-native overlay controls
0xCmdrKeen Sep 25, 2023
a0b0484
Set window.isFrameless regardless of method used
0xCmdrKeen Sep 25, 2023
6ab94cd
Use correct theme variables for window controls
0xCmdrKeen Sep 25, 2023
24df8c3
Change default window controls height to 39px
0xCmdrKeen Sep 25, 2023
a47d50b
Remove line checked in by mistake
0xCmdrKeen Sep 25, 2023
7f93bad
Use native overlay controls only on platforms that support it
0xCmdrKeen Sep 26, 2023
644f93d
Add 'fullscreen' class to App element when window is in fullscreen mode
0xCmdrKeen Sep 27, 2023
4dcd22c
Don't apply frameless fix if window is fullscreen
0xCmdrKeen Sep 27, 2023
e9d0adc
Fix window controls not appearing on Linux
0xCmdrKeen Sep 29, 2023
107ca30
Use more traditional icons for maximize/restore
0xCmdrKeen Sep 29, 2023
1b10433
Remove camel-case dependecy
0xCmdrKeen Sep 30, 2023
bc0b4c6
Replace `getWindowProps()` by `isFrameless()`
0xCmdrKeen Sep 30, 2023
927cc4a
Add experimental feature notification
0xCmdrKeen Oct 2, 2023
7eaaea1
Change tooltip to "Restore window" when maximized
0xCmdrKeen Oct 4, 2023
0946988
Merge branch 'main' into feature/frameless-window
0xCmdrKeen Oct 4, 2023
e576b98
Don't show frameless window option in Steam Deck Game Mode
0xCmdrKeen Oct 6, 2023
e31e030
Merge branch 'main' into feature/frameless-window
Etaash-mathamsetty Oct 29, 2023
e7bd9ac
Merge branch 'main' into feature/frameless-window
0xCmdrKeen Oct 31, 2023
0a5c2ac
Fix window controls scrolling with content
0xCmdrKeen Oct 31, 2023
f5d2c6f
Make WindowControls close button respect exitToTray setting
0xCmdrKeen Nov 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@
"experimental_features": {
"enableNewShinyFeature": "New shiny feature"
},
"frameless-window": "Use frameless window (restart required)",
"FsrSharpnessStrenght": "FSR Sharpness Strength",
"fsync": "Enable Fsync",
"gamemode": "Use GameMode (Feral Game Mode needs to be installed)",
Expand Down Expand Up @@ -708,6 +709,11 @@
"reload": "Reload page"
}
},
"window": {
"close": "Close",
"maximize": "Maximize window",
"minimize": "Minimize window"
},
"wine": {
"actions": "Action",
"manager": {
Expand Down
22 changes: 11 additions & 11 deletions src/backend/__tests__/constants.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { fixAsarPath } from '../constants'

export function overrideProcessPlatform(os: string): string {
const original_os = process.platform

// override process.platform
Object.defineProperty(process, 'platform', {
value: os
})

return original_os
}

jest.mock('../logger/logfile')

describe('Constants - fixAsarPath', () => {
Expand All @@ -15,17 +26,6 @@ describe('Constants - fixAsarPath', () => {
})

describe('Constants - getShell', () => {
function overrideProcessPlatform(os: string): string {
const original_os = process.platform

// override process.platform
Object.defineProperty(process, 'platform', {
value: os
})

return original_os
}

async function getShell(): Promise<string> {
jest.resetModules()
return import('../constants').then((module) => {
Expand Down
52 changes: 52 additions & 0 deletions src/backend/__tests__/main_window.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createMainWindow, sendFrontendMessage } from '../main_window'
import { BrowserWindow, Display, screen } from 'electron'
import { configStore } from '../constants'
import { overrideProcessPlatform } from './constants.test'

jest.mock('../logger/logfile')

Expand Down Expand Up @@ -106,5 +107,56 @@ describe('main_window', () => {
expect(options.y).toBe(0)
})
})

describe('with frameless window enabled', () => {
beforeEach(() => {
jest.spyOn(configStore, 'has').mockReturnValue(false)
jest.spyOn(configStore, 'get').mockReturnValue({
framelessWindow: true
})
})

it('creates a simple frameless window on Linux', () => {
const originalPlatform = overrideProcessPlatform('linux')
const window = createMainWindow()
const options = window['options']
overrideProcessPlatform(originalPlatform)

expect(options.frame).toBe(false)
expect(options.titleBarStyle).toBeUndefined()
expect(options.titleBarOverlay).toBeUndefined()
})

it('creates a frameless window with overlay controls on macOS and Windows', () => {
;['darwin', 'win32'].forEach((platform) => {
const originalPlatform = overrideProcessPlatform(platform)
const window = createMainWindow()
const options = window['options']
overrideProcessPlatform(originalPlatform)

expect(options.frame).toBeUndefined()
expect(options.titleBarStyle).toBe('hidden')
expect(options.titleBarOverlay).toBe(true)
})
})
})

describe('with frameless window disabled', () => {
beforeAll(() => {
jest.spyOn(configStore, 'has').mockReturnValue(false)
jest.spyOn(configStore, 'get').mockReturnValue({
framelessWindow: false
})
})

it('creates the new window with default titlebar', () => {
const window = createMainWindow()
const options = window['options']

expect(options.frame).toBeUndefined()
expect(options.titleBarStyle).toBeUndefined()
expect(options.titleBarOverlay).toBeUndefined()
})
})
})
})
5 changes: 5 additions & 0 deletions src/backend/__tests__/test_data/custom_titlebar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:root {
--titlebar-height: 40px;
--titlebar-color: #1a1b1c;
--titlebar-symbol-color: #fafbfc;
}
5 changes: 4 additions & 1 deletion src/backend/api/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron'
import { ipcRenderer, TitleBarOverlayOptions } from 'electron'
import {
Runner,
InstallPlatform,
Expand Down Expand Up @@ -112,6 +112,9 @@ export const getThemeCSS = async (theme: string) =>

export const getCustomThemes = async () => ipcRenderer.invoke('getCustomThemes')

export const setTitleBarOverlay = (options: TitleBarOverlayOptions) =>
ipcRenderer.send('setTitleBarOverlay', options)

export const isGameAvailable = async (args: {
appName: string
runner: Runner
Expand Down
30 changes: 30 additions & 0 deletions src/backend/api/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,36 @@ export const getCurrentChangelog = async () =>
export const openPatreonPage = () => ipcRenderer.send('openPatreonPage')
export const openKofiPage = () => ipcRenderer.send('openKofiPage')
export const isFullscreen = async () => ipcRenderer.invoke('isFullscreen')
export const isFrameless = async () => ipcRenderer.invoke('isFrameless')
export const isMinimized = async () => ipcRenderer.invoke('isMinimized')
export const isMaximized = async () => ipcRenderer.invoke('isMaximized')
export const minimizeWindow = () => ipcRenderer.send('minimizeWindow')
export const maximizeWindow = () => ipcRenderer.send('maximizeWindow')
export const unmaximizeWindow = () => ipcRenderer.send('unmaximizeWindow')
export const handleMaximized = (
callback: (e: Electron.IpcRendererEvent) => void
) => {
ipcRenderer.on('maximized', callback)
return () => {
ipcRenderer.removeListener('maximized', callback)
}
}
export const handleUnmaximized = (
callback: (e: Electron.IpcRendererEvent) => void
) => {
ipcRenderer.on('unmaximized', callback)
return () => {
ipcRenderer.removeListener('unmaximized', callback)
}
}
export const handleFullscreen = (
callback: (e: Electron.IpcRendererEvent, status: boolean) => void
) => {
ipcRenderer.on('fullscreen', callback)
return () => {
ipcRenderer.removeListener('fullscreen', callback)
}
}

export const openWebviewPage = (url: string) =>
ipcRenderer.send('openWebviewPage', url)
Expand Down
23 changes: 23 additions & 0 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import { initTrayIcon } from './tray_icon/tray_icon'
import {
createMainWindow,
getMainWindow,
isFrameless,
sendFrontendMessage
} from './main_window'

Expand Down Expand Up @@ -189,6 +190,14 @@ async function initializeWindow(): Promise<BrowserWindow> {
mainWindow.setIcon(icon)
app.commandLine.appendSwitch('enable-spatial-navigation')

mainWindow.on('maximize', () => sendFrontendMessage('maximized'))
mainWindow.on('unmaximize', () => sendFrontendMessage('unmaximized'))
mainWindow.on('enter-full-screen', () =>
sendFrontendMessage('fullscreen', true)
)
mainWindow.on('leave-full-screen', () =>
sendFrontendMessage('fullscreen', false)
)
mainWindow.on('close', async (e) => {
e.preventDefault()

Expand Down Expand Up @@ -513,6 +522,12 @@ ipcMain.handle('checkDiskSpace', async (event, folder) => {
})
})

ipcMain.handle('isFrameless', () => isFrameless())
ipcMain.handle('isMinimized', () => !!getMainWindow()?.isMinimized())
ipcMain.handle('isMaximized', () => !!getMainWindow()?.isMaximized())
ipcMain.on('minimizeWindow', () => getMainWindow()?.minimize())
ipcMain.on('maximizeWindow', () => getMainWindow()?.maximize())
ipcMain.on('unmaximizeWindow', () => getMainWindow()?.unmaximize())
ipcMain.on('quit', async () => handleExit())

// Quit when all windows are closed, except on macOS. There, it's common
Expand Down Expand Up @@ -1587,6 +1602,14 @@ ipcMain.handle('getThemeCSS', async (event, theme) => {
return readFileSync(cssPath, 'utf-8')
})

ipcMain.on('setTitleBarOverlay', (e, args) => {
const mainWindow = getMainWindow()
if (typeof mainWindow?.['setTitleBarOverlay'] === 'function') {
logDebug(`Setting titlebar overlay options ${JSON.stringify(args)}`)
mainWindow?.setTitleBarOverlay(args)
}
})

ipcMain.on('addNewApp', (e, args) => addNewApp(args))

ipcMain.handle('removeApp', async (e, args) => {
Expand Down
23 changes: 20 additions & 3 deletions src/backend/main_window.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WindowProps } from 'common/types'
import { AppSettings, WindowProps } from 'common/types'
import { BrowserWindow, screen } from 'electron'
import path from 'path'
import { configStore } from './constants'
Expand All @@ -9,6 +9,12 @@ export const getMainWindow = () => {
return mainWindow
}

let windowProps: WindowProps | null = null

export const isFrameless = () => {
return windowProps?.frame === false || windowProps?.titleBarStyle === 'hidden'
}

// send a message to the main window's webContents if available
// returns `false` if no mainWindow or no webContents
// returns `true` if the message was sent to the webContents
Expand All @@ -29,13 +35,13 @@ export const sendFrontendMessage = (message: string, ...payload: unknown[]) => {

// creates the mainWindow based on the configuration
export const createMainWindow = () => {
let windowProps: WindowProps = {
windowProps = {
height: 690,
width: 1200,
x: 0,
y: 0,
maximized: false
}
} as WindowProps

if (configStore.has('window-props')) {
windowProps = configStore.get('window-props', windowProps)
Expand All @@ -51,6 +57,17 @@ export const createMainWindow = () => {
windowProps.width = screenInfo.workAreaSize.width * 0.8
}
}
// Set up frameless window if enabled in settings
const settings = configStore.get('settings', <AppSettings>{})
if (settings.framelessWindow) {
// use native overlay controls where supported
if (['darwin', 'win32'].includes(process.platform)) {
windowProps.titleBarStyle = 'hidden'
windowProps.titleBarOverlay = true
} else {
windowProps.frame = false
}
}
const { maximized, ...props } = windowProps
// Create the browser window.
mainWindow = new BrowserWindow({
Expand Down
13 changes: 12 additions & 1 deletion src/common/typedefs/ipcBridge.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { DownloadManagerState } from './../types'
import { EventEmitter } from 'node:events'
import { IpcMainEvent, OpenDialogOptions } from 'electron'
import {
IpcMainEvent,
OpenDialogOptions,
TitleBarOverlayOptions
} from 'electron'

import {
Runner,
Expand Down Expand Up @@ -105,6 +109,10 @@ interface SyncIPCFunctions {
pauseCurrentDownload: () => void
cancelDownload: (removeDownloaded: boolean) => void
copySystemInfoToClipboard: () => void
minimizeWindow: () => void
maximizeWindow: () => void
unmaximizeWindow: () => void
setTitleBarOverlay: (options: TitleBarOverlayOptions) => void
}

// ts-prune-ignore-next
Expand All @@ -125,6 +133,9 @@ interface AsyncIPCFunctions {
getGogdlVersion: () => Promise<string>
getNileVersion: () => Promise<string>
isFullscreen: () => boolean
isFrameless: () => boolean
isMaximized: () => boolean
isMinimized: () => boolean
isFlatpak: () => boolean
getPlatform: () => NodeJS.Platform
showUpdateSetting: () => boolean
Expand Down
6 changes: 5 additions & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GOGCloudSavesLocation, GogInstallPlatform } from './types/gog'
import { LegendaryInstallPlatform, GameMetadataInner } from './types/legendary'
import { IpcRendererEvent } from 'electron'
import { IpcRendererEvent, TitleBarOverlay } from 'electron'
import { ChildProcess } from 'child_process'
import { HowLongToBeatEntry } from 'howlongtobeat'
import { NileInstallPlatform } from './types/nile'
Expand Down Expand Up @@ -71,6 +71,7 @@ export interface AppSettings extends GameSettings {
enableUpdates: boolean
exitToTray: boolean
experimentalFeatures: ExperimentalFeatures
framelessWindow: boolean
hideChangelogsOnStartup: boolean
libraryTopSection: LibraryTopSectionOptions
maxRecentGames: number
Expand Down Expand Up @@ -703,4 +704,7 @@ export type DownloadManagerState = 'idle' | 'running' | 'paused' | 'stopped'

export interface WindowProps extends Electron.Rectangle {
maximized: boolean
frame?: boolean
titleBarStyle?: 'default' | 'hidden' | 'hiddenInset'
titleBarOverlay?: TitleBarOverlay | boolean
}
12 changes: 10 additions & 2 deletions src/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ import DownloadManager from './screens/DownloadManager'
import DialogHandler from './components/UI/DialogHandler'
import SettingsModal from './screens/Settings/components/SettingsModal'
import ExternalLinkDialog from './components/UI/ExternalLinkDialog'
import WindowControls from './components/UI/WindowControls'
import classNames from 'classnames'

function App() {
const { isSettingsModalOpen, isRTL } = useContext(ContextProvider)
const { isSettingsModalOpen, isRTL, isFullscreen, isFrameless } =
useContext(ContextProvider)

const hasNativeOverlayControls = navigator['windowControlsOverlay']?.visible
const showOverlayControls = isFrameless && !hasNativeOverlayControls

return (
<div
id="app"
className={classNames('App', {
isRTL
isRTL,
frameless: isFrameless,
fullscreen: isFullscreen
})}
>
<HashRouter>
Expand Down Expand Up @@ -73,6 +80,7 @@ function App() {
<ControllerHints />
<div className="simple-keyboard"></div>
</div>
{showOverlayControls && <WindowControls />}
</HashRouter>
</div>
)
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/components/UI/Header/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
color: var(--text-secondary);
}

.frameless:not(.fullscreen) .Header {
padding-right: calc(
var(--overlay-controls-width) - env(titlebar-area-x, 0px)
);
}

@media screen and (max-width: 1000px) {
.Header {
grid-template-columns: 1fr min-content;
Expand Down
Loading
Loading