Compare commits

...

2 Commits

Author SHA1 Message Date
Clément Pasteau
c8477629d0 Improvements of full screen mode on mac + rework of callbacks 2025-01-14 15:13:30 +01:00
Clément Pasteau
08b0a9ff37 Improve title bar on multiple platforms optimizing the space at the top of the app 2025-01-14 11:04:57 +01:00
5 changed files with 212 additions and 130 deletions

View File

@@ -3,24 +3,27 @@ import * as React from 'react';
import MenuIcon from '../UI/CustomSvgIcons/Menu'; import MenuIcon from '../UI/CustomSvgIcons/Menu';
import IconButton from '../UI/IconButton'; import IconButton from '../UI/IconButton';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import optionalRequire from '../Utils/OptionalRequire'; import Window from '../Utils/Window';
import { isMacLike } from '../Utils/Platform'; import {
import Window, { useWindowControlsOverlayWatcher } from '../Utils/Window'; TitleBarLeftSafeMargins,
import useForceUpdate from '../Utils/UseForceUpdate'; TitleBarRightSafeMargins,
const electron = optionalRequire('electron'); } from '../UI/TitleBarSafeMargins';
type Props = {| type Props = {|
children: React.Node, children: React.Node,
toggleProjectManager: () => void, toggleProjectManager: () => void,
|}; |};
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
const styles = { const styles = {
container: { display: 'flex', flexShrink: 0, alignItems: 'flex-end' }, container: { display: 'flex', flexShrink: 0, alignItems: 'flex-end' },
leftSideArea: { alignSelf: 'stretch', flexShrink: 0 }, menuIcon: {
rightSideArea: { alignSelf: 'stretch', flex: 1 }, marginLeft: 4,
menuIcon: { marginLeft: 4, marginRight: 4 }, marginRight: 4,
// Make the icon slightly bigger to be centered on the row, so it aligns
// with the project manager icon.
width: 34,
height: 34,
},
}; };
/** /**
@@ -30,7 +33,6 @@ export default function TabsTitlebar({
children, children,
toggleProjectManager, toggleProjectManager,
}: Props) { }: Props) {
const forceUpdate = useForceUpdate();
const gdevelopTheme = React.useContext(GDevelopThemeContext); const gdevelopTheme = React.useContext(GDevelopThemeContext);
const backgroundColor = gdevelopTheme.titlebar.backgroundColor; const backgroundColor = gdevelopTheme.titlebar.backgroundColor;
React.useEffect( React.useEffect(
@@ -40,41 +42,9 @@ export default function TabsTitlebar({
[backgroundColor] [backgroundColor]
); );
// An installed PWA can have window controls displayed as overlay. If supported,
// we set up a listener to detect any change and force a refresh that will read
// the latest size of the controls.
useWindowControlsOverlayWatcher({ onChanged: forceUpdate });
// macOS displays the "traffic lights" on the left.
const isDesktopMacos = !!electron && isMacLike();
let leftSideOffset = isDesktopMacos ? 76 : 0;
const isDesktopWindowsOrLinux = !!electron && !isMacLike();
// Windows and Linux have their "window controls" on the right
let rightSideOffset = isDesktopWindowsOrLinux ? 150 : 0;
// An installed PWA can have window controls displayed as overlay,
// which we measure here to set the offsets.
// $FlowFixMe - this API is not handled by Flow.
const { windowControlsOverlay } = navigator;
if (windowControlsOverlay) {
if (windowControlsOverlay.visible) {
const { x, width } = windowControlsOverlay.getTitlebarAreaRect();
leftSideOffset = x;
rightSideOffset = window.innerWidth - x - width;
}
}
const rightSideAdditionalOffsetToGiveSpaceToDrag = 30;
return ( return (
<div style={{ ...styles.container, backgroundColor }}> <div style={{ ...styles.container, backgroundColor }}>
<div <TitleBarLeftSafeMargins />
style={{
...styles.leftSideArea,
width: leftSideOffset,
}}
className={DRAGGABLE_PART_CLASS_NAME}
/>
<IconButton <IconButton
size="small" size="small"
// Even if not in the toolbar, keep this ID for backward compatibility for tutorials. // Even if not in the toolbar, keep this ID for backward compatibility for tutorials.
@@ -86,14 +56,7 @@ export default function TabsTitlebar({
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
{children} {children}
<div <TitleBarRightSafeMargins />
style={{
...styles.rightSideArea,
minWidth:
rightSideOffset + rightSideAdditionalOffsetToGiveSpaceToDrag,
}}
className={DRAGGABLE_PART_CLASS_NAME}
/>
</div> </div>
); );
} }

View File

@@ -21,48 +21,16 @@ import Cross from './CustomSvgIcons/Cross';
import IconButton from './IconButton'; import IconButton from './IconButton';
import { Line } from './Grid'; import { Line } from './Grid';
import GDevelopThemeContext from './Theme/GDevelopThemeContext'; import GDevelopThemeContext from './Theme/GDevelopThemeContext';
import optionalRequire from '../Utils/OptionalRequire';
import useForceUpdate from '../Utils/UseForceUpdate';
import { useWindowControlsOverlayWatcher } from '../Utils/Window';
import { classNameToStillAllowRenderingInstancesEditor } from './MaterialUISpecificUtil'; import { classNameToStillAllowRenderingInstancesEditor } from './MaterialUISpecificUtil';
import { import {
getAvoidSoftKeyboardStyle, getAvoidSoftKeyboardStyle,
useSoftKeyboardBottomOffset, useSoftKeyboardBottomOffset,
} from './MobileSoftKeyboard'; } from './MobileSoftKeyboard';
import {
const electron = optionalRequire('electron'); TitleBarLeftSafeMargins,
TitleBarRightSafeMargins,
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part'; } from './TitleBarSafeMargins';
export const DialogTitleBar = ({
backgroundColor,
}: {|
backgroundColor: string,
|}) => {
// An installed PWA can have window controls displayed as overlay. If supported,
// we set up a listener to detect any change and force a refresh that will read
// the latest size of the controls.
const forceUpdate = useForceUpdate();
useWindowControlsOverlayWatcher({ onChanged: forceUpdate });
// $FlowFixMe - this API is not handled by Flow.
const { windowControlsOverlay } = navigator;
if (!!electron || (windowControlsOverlay && windowControlsOverlay.visible)) {
// We're on the desktop app, or in an installed PWA with window controls displayed
// as overlay: we need to display a spacing at the top of the dialog.
return (
<div
className={DRAGGABLE_PART_CLASS_NAME}
style={{ height: 38, backgroundColor, flexShrink: 0 }}
/>
);
}
// Not on the desktop app, and not in an installed PWA with window controls displayed
// as overlay: no need to display a spacing.
return null;
};
// Default. // Default.
const dialogPaddingX = 24; const dialogPaddingX = 24;
@@ -110,6 +78,11 @@ const styles = {
// Ensure the title can break on any character, to ensure it's visible on mobile. Especially useful for long emails. // Ensure the title can break on any character, to ensure it's visible on mobile. Especially useful for long emails.
overflowWrap: 'anywhere', overflowWrap: 'anywhere',
}, },
closeDialogContainer: {
// Ensure this part can be interacted with on macOS, when the dialog is fullscreen and used as PWA.
// Otherwise, the close button is not clickable.
WebkitAppRegion: 'no-drag',
},
fixedContentContainer: { fixedContentContainer: {
paddingBottom: 8, paddingBottom: 8,
}, },
@@ -402,11 +375,6 @@ const Dialog = ({
disableBackdropClick={false} disableBackdropClick={false}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
{isFullScreen && (
<DialogTitleBar
backgroundColor={gdevelopTheme.titlebar.backgroundColor}
/>
)}
<div style={dialogContainerStyle}> <div style={dialogContainerStyle}>
<div <div
style={{ style={{
@@ -415,18 +383,34 @@ const Dialog = ({
}} }}
> >
<Line noMargin justifyContent="space-between" alignItems="flex-start"> <Line noMargin justifyContent="space-between" alignItems="flex-start">
<Text noMargin size="section-title"> <Line noMargin alignItems="center">
{title} {isFullScreen && (
</Text> <TitleBarLeftSafeMargins
{onRequestClose && !cannotBeDismissed && ( backgroundColor={gdevelopTheme.dialog.backgroundColor}
<IconButton />
onClick={onRequestClose} )}
size="small" <Text noMargin size="section-title">
disabled={cannotBeDismissed} {title}
> </Text>
<Cross /> </Line>
</IconButton> <Line noMargin alignItems="center">
)} {onRequestClose && !cannotBeDismissed && (
<div style={styles.closeDialogContainer}>
<IconButton
onClick={onRequestClose}
size="small"
disabled={cannotBeDismissed}
>
<Cross />
</IconButton>
</div>
)}
{isFullScreen && (
<TitleBarRightSafeMargins
backgroundColor={gdevelopTheme.dialog.backgroundColor}
/>
)}
</Line>
</Line> </Line>
</div> </div>
{fixedContent && ( {fixedContent && (

View File

@@ -7,10 +7,10 @@ import Typography from '@material-ui/core/Typography';
import Cross from './CustomSvgIcons/Cross'; import Cross from './CustomSvgIcons/Cross';
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from '@material-ui/core/Tooltip';
import { tooltipEnterDelay } from './Tooltip'; import { tooltipEnterDelay } from './Tooltip';
import { DialogTitleBar } from '../UI/Dialog';
import { LineStackLayout } from './Layout'; import { LineStackLayout } from './Layout';
import { TitleBarLeftSafeMargins } from './TitleBarSafeMargins';
const appBarHeight = 32; const appBarHeight = 38;
const styles = { const styles = {
appBar: { appBar: {
@@ -20,8 +20,11 @@ const styles = {
toolbar: { toolbar: {
height: appBarHeight, height: appBarHeight,
minHeight: appBarHeight, minHeight: appBarHeight,
paddingLeft: 15, paddingLeft: 8,
paddingRight: 15, paddingRight: 8,
// Ensure this part can be interacted with on macOS, when used as PWA.
// Otherwise, the buttons are not clickable.
WebkitAppRegion: 'no-drag',
}, },
title: { title: {
fontSize: '15px', fontSize: '15px',
@@ -42,7 +45,6 @@ type Props = {|
const DrawerTopBar = (props: Props) => { const DrawerTopBar = (props: Props) => {
return ( return (
<> <>
<DialogTitleBar backgroundColor="transparent" />
<AppBar <AppBar
position="static" position="static"
style={styles.appBar} style={styles.appBar}
@@ -51,6 +53,7 @@ const DrawerTopBar = (props: Props) => {
elevation={0} elevation={0}
> >
<Toolbar style={styles.toolbar}> <Toolbar style={styles.toolbar}>
<TitleBarLeftSafeMargins />
<LineStackLayout noMargin expand alignItems="center"> <LineStackLayout noMargin expand alignItems="center">
{props.icon && ( {props.icon && (
<IconButton <IconButton

View File

@@ -0,0 +1,115 @@
// @flow
import * as React from 'react';
import { isMacLike } from '../Utils/Platform';
import useForceUpdate from '../Utils/UseForceUpdate';
import Window, { useWindowControlsOverlayWatcher } from '../Utils/Window';
import optionalRequire from '../Utils/OptionalRequire';
const electron = optionalRequire('electron');
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
const titleBarStyles = {
leftSideArea: {
alignSelf: 'stretch',
flexShrink: 0,
},
rightSideArea: { alignSelf: 'stretch', flex: 1 },
};
export const TitleBarLeftSafeMargins = ({
backgroundColor,
}: {|
backgroundColor?: string,
|}) => {
// An installed PWA can have window controls displayed as overlay. If supported,
// we set up a listener to detect any change and force a refresh that will read
// the latest size of the controls.
const forceUpdate = useForceUpdate();
useWindowControlsOverlayWatcher({ onChanged: forceUpdate });
let leftSideOffset = 0;
// macOS displays the "traffic lights" on the left.
const isDesktopMacosFullScreen =
!!electron && isMacLike() && Window.isFullScreen();
if (isDesktopMacosFullScreen) {
// When in full screen on macOS, the "traffic lights" are not in the overlay.
leftSideOffset = 0;
} else {
// Otherwise, the windowControlsOverlay tells us how much space is needed.
// This can happen for mac apps, or installed PWA.
// $FlowFixMe - this API is not handled by Flow.
const { windowControlsOverlay } = navigator;
if (windowControlsOverlay) {
if (windowControlsOverlay.visible) {
const { x } = windowControlsOverlay.getTitlebarAreaRect();
leftSideOffset = x;
}
}
}
if (leftSideOffset) {
return (
<div
className={DRAGGABLE_PART_CLASS_NAME}
style={{
...titleBarStyles.leftSideArea,
width: leftSideOffset,
backgroundColor: backgroundColor || 'transparent',
}}
/>
);
}
// Not on the desktop app, and not in an installed PWA with window controls displayed
// as overlay: no need to display a spacing.
return null;
};
export const TitleBarRightSafeMargins = ({
backgroundColor,
}: {|
backgroundColor?: string,
|}) => {
// An installed PWA can have window controls displayed as overlay. If supported,
// we set up a listener to detect any change and force a refresh that will read
// the latest size of the controls.
const forceUpdate = useForceUpdate();
useWindowControlsOverlayWatcher({ onChanged: forceUpdate });
const isDesktopWindowsOrLinux = !!electron && !isMacLike();
// Windows and Linux have their "window controls" on the right
let rightSideOffset = isDesktopWindowsOrLinux ? 150 : 0;
// An installed PWA can have window controls displayed as overlay,
// which we measure here to set the offsets.
// $FlowFixMe - this API is not handled by Flow.
const { windowControlsOverlay } = navigator;
if (windowControlsOverlay) {
if (windowControlsOverlay.visible) {
const { x, width } = windowControlsOverlay.getTitlebarAreaRect();
rightSideOffset = window.innerWidth - x - width;
}
}
const rightSideAdditionalOffsetToGiveSpaceToDrag = 30;
if (rightSideOffset) {
return (
<div
className={DRAGGABLE_PART_CLASS_NAME}
style={{
...titleBarStyles.rightSideArea,
minWidth:
rightSideOffset + rightSideAdditionalOffsetToGiveSpaceToDrag,
backgroundColor: backgroundColor || 'transparent',
}}
/>
);
}
// Not on the desktop app, and not in an installed PWA with window controls displayed
// as overlay: no need to display a spacing.
return null;
};

View File

@@ -22,6 +22,31 @@ export const POSITIONAL_ARGUMENTS_KEY = '_';
let currentTitleBarColor: ?string = null; let currentTitleBarColor: ?string = null;
const onChangeCallbacks = new Set<() => void>();
let debouncedGeometryChange = null;
const setupWindowControlsOverlayWatcher = () => {
if (debouncedGeometryChange) {
// Already set up.
return;
}
// $FlowFixMe - this API is not handled by Flow.
const { windowControlsOverlay } = navigator;
if (windowControlsOverlay) {
debouncedGeometryChange = debounce(() => {
for (const callback of onChangeCallbacks) {
callback();
}
}, 20);
windowControlsOverlay.addEventListener(
'geometrychange',
debouncedGeometryChange
);
}
};
/** /**
* Listen to the changes to the window controls provided by the operating system: * Listen to the changes to the window controls provided by the operating system:
* *
@@ -34,31 +59,16 @@ export const useWindowControlsOverlayWatcher = ({
}: {| }: {|
onChanged: () => void, onChanged: () => void,
|}) => { |}) => {
// $FlowFixMe - this API is not handled by Flow. setupWindowControlsOverlayWatcher();
const { windowControlsOverlay } = navigator;
React.useEffect( React.useEffect(
() => { () => {
let listenerCallback = null; onChangeCallbacks.add(onChanged);
if (windowControlsOverlay) {
listenerCallback = debounce(() => {
onChanged();
}, 50);
windowControlsOverlay.addEventListener(
'geometrychange',
listenerCallback
);
}
return () => { return () => {
if (listenerCallback) { onChangeCallbacks.delete(onChanged);
windowControlsOverlay.removeEventListener(
'geometrychange',
listenerCallback
);
}
}; };
}, },
[onChanged, windowControlsOverlay] [onChanged]
); );
}; };
@@ -372,4 +382,11 @@ export default class Window {
return false; // Assume we're not in development mode. Might be incorrect but better not consider production as development. return false; // Assume we're not in development mode. Might be incorrect but better not consider production as development.
} }
} }
static isFullScreen(): boolean {
if (!remote) return false;
const browserWindow = remote.getCurrentWindow();
return browserWindow.isFullScreen();
}
} }