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 IconButton from '../UI/IconButton';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import optionalRequire from '../Utils/OptionalRequire';
import { isMacLike } from '../Utils/Platform';
import Window, { useWindowControlsOverlayWatcher } from '../Utils/Window';
import useForceUpdate from '../Utils/UseForceUpdate';
const electron = optionalRequire('electron');
import Window from '../Utils/Window';
import {
TitleBarLeftSafeMargins,
TitleBarRightSafeMargins,
} from '../UI/TitleBarSafeMargins';
type Props = {|
children: React.Node,
toggleProjectManager: () => void,
|};
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
const styles = {
container: { display: 'flex', flexShrink: 0, alignItems: 'flex-end' },
leftSideArea: { alignSelf: 'stretch', flexShrink: 0 },
rightSideArea: { alignSelf: 'stretch', flex: 1 },
menuIcon: { marginLeft: 4, marginRight: 4 },
menuIcon: {
marginLeft: 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,
toggleProjectManager,
}: Props) {
const forceUpdate = useForceUpdate();
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const backgroundColor = gdevelopTheme.titlebar.backgroundColor;
React.useEffect(
@@ -40,41 +42,9 @@ export default function TabsTitlebar({
[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 (
<div style={{ ...styles.container, backgroundColor }}>
<div
style={{
...styles.leftSideArea,
width: leftSideOffset,
}}
className={DRAGGABLE_PART_CLASS_NAME}
/>
<TitleBarLeftSafeMargins />
<IconButton
size="small"
// Even if not in the toolbar, keep this ID for backward compatibility for tutorials.
@@ -86,14 +56,7 @@ export default function TabsTitlebar({
<MenuIcon />
</IconButton>
{children}
<div
style={{
...styles.rightSideArea,
minWidth:
rightSideOffset + rightSideAdditionalOffsetToGiveSpaceToDrag,
}}
className={DRAGGABLE_PART_CLASS_NAME}
/>
<TitleBarRightSafeMargins />
</div>
);
}

View File

@@ -21,48 +21,16 @@ import Cross from './CustomSvgIcons/Cross';
import IconButton from './IconButton';
import { Line } from './Grid';
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 {
getAvoidSoftKeyboardStyle,
useSoftKeyboardBottomOffset,
} from './MobileSoftKeyboard';
const electron = optionalRequire('electron');
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
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;
};
import {
TitleBarLeftSafeMargins,
TitleBarRightSafeMargins,
} from './TitleBarSafeMargins';
// Default.
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.
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: {
paddingBottom: 8,
},
@@ -402,11 +375,6 @@ const Dialog = ({
disableBackdropClick={false}
onKeyDown={handleKeyDown}
>
{isFullScreen && (
<DialogTitleBar
backgroundColor={gdevelopTheme.titlebar.backgroundColor}
/>
)}
<div style={dialogContainerStyle}>
<div
style={{
@@ -415,18 +383,34 @@ const Dialog = ({
}}
>
<Line noMargin justifyContent="space-between" alignItems="flex-start">
<Text noMargin size="section-title">
{title}
</Text>
{onRequestClose && !cannotBeDismissed && (
<IconButton
onClick={onRequestClose}
size="small"
disabled={cannotBeDismissed}
>
<Cross />
</IconButton>
)}
<Line noMargin alignItems="center">
{isFullScreen && (
<TitleBarLeftSafeMargins
backgroundColor={gdevelopTheme.dialog.backgroundColor}
/>
)}
<Text noMargin size="section-title">
{title}
</Text>
</Line>
<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>
</div>
{fixedContent && (

View File

@@ -7,10 +7,10 @@ import Typography from '@material-ui/core/Typography';
import Cross from './CustomSvgIcons/Cross';
import Tooltip from '@material-ui/core/Tooltip';
import { tooltipEnterDelay } from './Tooltip';
import { DialogTitleBar } from '../UI/Dialog';
import { LineStackLayout } from './Layout';
import { TitleBarLeftSafeMargins } from './TitleBarSafeMargins';
const appBarHeight = 32;
const appBarHeight = 38;
const styles = {
appBar: {
@@ -20,8 +20,11 @@ const styles = {
toolbar: {
height: appBarHeight,
minHeight: appBarHeight,
paddingLeft: 15,
paddingRight: 15,
paddingLeft: 8,
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: {
fontSize: '15px',
@@ -42,7 +45,6 @@ type Props = {|
const DrawerTopBar = (props: Props) => {
return (
<>
<DialogTitleBar backgroundColor="transparent" />
<AppBar
position="static"
style={styles.appBar}
@@ -51,6 +53,7 @@ const DrawerTopBar = (props: Props) => {
elevation={0}
>
<Toolbar style={styles.toolbar}>
<TitleBarLeftSafeMargins />
<LineStackLayout noMargin expand alignItems="center">
{props.icon && (
<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;
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:
*
@@ -34,31 +59,16 @@ export const useWindowControlsOverlayWatcher = ({
}: {|
onChanged: () => void,
|}) => {
// $FlowFixMe - this API is not handled by Flow.
const { windowControlsOverlay } = navigator;
setupWindowControlsOverlayWatcher();
React.useEffect(
() => {
let listenerCallback = null;
if (windowControlsOverlay) {
listenerCallback = debounce(() => {
onChanged();
}, 50);
windowControlsOverlay.addEventListener(
'geometrychange',
listenerCallback
);
}
onChangeCallbacks.add(onChanged);
return () => {
if (listenerCallback) {
windowControlsOverlay.removeEventListener(
'geometrychange',
listenerCallback
);
}
onChangeCallbacks.delete(onChanged);
};
},
[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.
}
}
static isFullScreen(): boolean {
if (!remote) return false;
const browserWindow = remote.getCurrentWindow();
return browserWindow.isFullScreen();
}
}