diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 88ba4f7..3614b88 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -190,6 +190,27 @@ html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card { background: rgba(31, 31, 31, 0.5) !important; } +.redTrafficLight { + width: 12px; + height: 12px; + background-color: #fe5f57; + border-radius: 50%; +} + +.yellowTrafficLight { + width: 12px; + height: 12px; + background-color: #febc2e; + border-radius: 50%; +} + +.greenTrafficLight { + width: 12px; + height: 12px; + background-color: #28c840; + border-radius: 50%; +} + .electron-navigation { line-height: 40px; } diff --git a/assets/trafficlights/closecolored.svg b/assets/trafficlights/closecolored.svg new file mode 100644 index 0000000..7aead9a --- /dev/null +++ b/assets/trafficlights/closecolored.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/trafficlights/closedowncolored.svg b/assets/trafficlights/closedowncolored.svg new file mode 100644 index 0000000..707b68f --- /dev/null +++ b/assets/trafficlights/closedowncolored.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/trafficlights/closehovercolored.svg b/assets/trafficlights/closehovercolored.svg new file mode 100644 index 0000000..3c0db94 --- /dev/null +++ b/assets/trafficlights/closehovercolored.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/trafficlights/exitfullscreendowncolored.svg b/assets/trafficlights/exitfullscreendowncolored.svg new file mode 100644 index 0000000..62abef8 --- /dev/null +++ b/assets/trafficlights/exitfullscreendowncolored.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/trafficlights/exitfullscreenhovercolored.svg b/assets/trafficlights/exitfullscreenhovercolored.svg new file mode 100644 index 0000000..177210b --- /dev/null +++ b/assets/trafficlights/exitfullscreenhovercolored.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/trafficlights/fullscreencolored.svg b/assets/trafficlights/fullscreencolored.svg new file mode 100644 index 0000000..c098917 --- /dev/null +++ b/assets/trafficlights/fullscreencolored.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/trafficlights/fullscreendowncolored.svg b/assets/trafficlights/fullscreendowncolored.svg new file mode 100644 index 0000000..e9dd466 --- /dev/null +++ b/assets/trafficlights/fullscreendowncolored.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/trafficlights/fullscreenhovercolored.svg b/assets/trafficlights/fullscreenhovercolored.svg new file mode 100644 index 0000000..38ef153 --- /dev/null +++ b/assets/trafficlights/fullscreenhovercolored.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/trafficlights/minimizecolored.svg b/assets/trafficlights/minimizecolored.svg new file mode 100644 index 0000000..480df43 --- /dev/null +++ b/assets/trafficlights/minimizecolored.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/trafficlights/minimizedowncolored.svg b/assets/trafficlights/minimizedowncolored.svg new file mode 100644 index 0000000..f2ff3e4 --- /dev/null +++ b/assets/trafficlights/minimizedowncolored.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/trafficlights/minimizehovercolored.svg b/assets/trafficlights/minimizehovercolored.svg new file mode 100644 index 0000000..6cad028 --- /dev/null +++ b/assets/trafficlights/minimizehovercolored.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/trafficlights/nofocus.svg b/assets/trafficlights/nofocus.svg new file mode 100644 index 0000000..79efe5e --- /dev/null +++ b/assets/trafficlights/nofocus.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/native/macos/window-effects.mm b/native/macos/window-effects.mm index 1898603..04ddaac 100644 --- a/native/macos/window-effects.mm +++ b/native/macos/window-effects.mm @@ -106,6 +106,7 @@ static const void *kElectrobunChromeObserverKey = &kElectrobunChromeObserverKey; static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x, CGFloat yFromTop); static void refreshWindowChrome(NSWindow *window); +static BOOL isWindowFullScreen(NSWindow *window); @implementation ElectrobunWindowChromeObserver @@ -134,6 +135,10 @@ static void refreshWindowChrome(NSWindow *window); selector:@selector(refreshChrome) name:NSWindowDidBecomeKeyNotification object:window]; + [center addObserver:self + selector:@selector(refreshChrome) + name:NSWindowWillEnterFullScreenNotification + object:window]; } return self; } diff --git a/public/mainWindow.js b/public/mainWindow.js index 0dd67d2..223c7f8 100644 --- a/public/mainWindow.js +++ b/public/mainWindow.js @@ -265,6 +265,9 @@ export function setupMainWindowIPC() { case 'close': win.close() break + case 'fullscreen': + win.setFullScreen(!win.isFullScreen()) + break default: break } diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index 9a4c4c4..2965b9d 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -64,7 +64,7 @@ const DashboardNavigation = () => { icon: }) const isMobile = useMediaQuery({ maxWidth: 768 }) - const { platform, isElectron, isFullScreen, setSidebarViewMenu } = + const { platform, isElectron, setSidebarViewMenu } = useContext(ElectronContext) const { availableUpdate, checkForUpdates } = useAppUpdateContext() const mainMenuItems = useMemo( @@ -140,9 +140,7 @@ const DashboardNavigation = () => { setSidebarViewMenu(sections) }, [isElectron, setSidebarViewMenu]) - const showAppLogo = - (isElectron && platform == 'darwin' && isFullScreen == true) || - (isElectron && platform != 'darwin') + const showAppLogo = isElectron && platform != 'darwin' const isMacOSApp = isElectron && platform == 'darwin' const isOtherApp = isElectron && platform != 'darwin' diff --git a/src/components/Dashboard/common/DashboardWindowButtons.jsx b/src/components/Dashboard/common/DashboardWindowButtons.jsx index 157c52f..2a4a32e 100644 --- a/src/components/Dashboard/common/DashboardWindowButtons.jsx +++ b/src/components/Dashboard/common/DashboardWindowButtons.jsx @@ -5,6 +5,7 @@ import XMarkIcon from '../../Icons/XMarkIcon' import MinusIcon from '../../Icons/MinusIcon' import ContractIcon from '../../Icons/ContractIcon' import ExpandIcon from '../../Icons/ExpandIcon' +import MacOSTrafficLights from './MacOSTrafficLights' const DashboardWindowButtons = () => { const { isMaximized, handleWindowControl, platform, isFullScreen } = @@ -17,14 +18,21 @@ const DashboardWindowButtons = () => { onClick={() => handleWindowControl('close')} /> ) - const maximizeButton = ( + const minimizeButton = ( } type={'text'} onClick={() => handleWindowControl('minimize')} /> ) - const minimizeButton = ( + const fullscreenButton = ( + : } + type={'text'} + onClick={() => handleWindowControl('fullscreen')} + /> + ) + const maximizeButton = ( : } type={'text'} @@ -32,29 +40,41 @@ const DashboardWindowButtons = () => { /> ) - return ( - - {platform == 'darwin' ? ( - isFullScreen == false ? ( + const customWindowControls = ( + + + {platform == 'darwin' ? ( <> - - + {closeButton} + {minimizeButton} + {fullscreenButton} > - ) : null - ) : ( - - + ) : ( + <> {maximizeButton} {minimizeButton} {closeButton} - - + > + )} + + + ) + + return ( + + {platform == 'darwin' ? ( + <> + + + > + ) : ( + customWindowControls )} ) diff --git a/src/components/Dashboard/common/MacOSTrafficLights.jsx b/src/components/Dashboard/common/MacOSTrafficLights.jsx new file mode 100644 index 0000000..c615c73 --- /dev/null +++ b/src/components/Dashboard/common/MacOSTrafficLights.jsx @@ -0,0 +1,212 @@ +import { useContext, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { Flex } from 'antd' +import { ElectronContext } from '../context/ElectronContext' + +import CloseColored from '../../../../assets/trafficlights/closecolored.svg?react' +import CloseHoverColored from '../../../../assets/trafficlights/closehovercolored.svg?react' +import CloseDownColored from '../../../../assets/trafficlights/closedowncolored.svg?react' +import MinimizeColored from '../../../../assets/trafficlights/minimizecolored.svg?react' +import MinimizeHoverColored from '../../../../assets/trafficlights/minimizehovercolored.svg?react' +import MinimizeDownColored from '../../../../assets/trafficlights/minimizedowncolored.svg?react' +import FullscreenColored from '../../../../assets/trafficlights/fullscreencolored.svg?react' +import FullscreenHoverColored from '../../../../assets/trafficlights/fullscreenhovercolored.svg?react' +import FullscreenDownColored from '../../../../assets/trafficlights/fullscreendowncolored.svg?react' +import ExitFullscreenHoverColored from '../../../../assets/trafficlights/exitfullscreenhovercolored.svg?react' +import ExitFullscreenDownColored from '../../../../assets/trafficlights/exitfullscreendowncolored.svg?react' +import NoFocus from '../../../../assets/trafficlights/nofocus.svg?react' + +const TRAFFIC_LIGHT_SIZE = 12 + +const TRAFFIC_LIGHT_BUTTON_STYLE = { + width: TRAFFIC_LIGHT_SIZE, + height: TRAFFIC_LIGHT_SIZE, + padding: 0, + border: 'none', + background: 'transparent', + cursor: 'default', + lineHeight: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' +} + +const TRAFFIC_LIGHT_ICON_STYLE = { + width: TRAFFIC_LIGHT_SIZE, + height: TRAFFIC_LIGHT_SIZE, + display: 'block' +} + +function getTrafficLightIcon({ isEnabled, isFocused, isActive, isHovered, icons }) { + if (!isEnabled || !isFocused) { + return icons.noFocus + } + if (isActive) { + return icons.down + } + if (isHovered) { + return icons.hover + } + return icons.colored +} + +const TrafficLightButton = ({ + action, + icons, + isGroupHovered, + activeAction, + isEnabled, + isWindowFocused, + onActive, + onAction +}) => { + const Icon = getTrafficLightIcon({ + isEnabled, + isFocused: isWindowFocused, + isActive: activeAction === action, + isHovered: isGroupHovered, + icons + }) + + const handleMouseDown = (event) => { + if (!isEnabled) return + + event.preventDefault() + event.stopPropagation() + onActive(action) + onAction(action) + } + + return ( + onActive(null) : undefined} + > + + + ) +} + +TrafficLightButton.propTypes = { + action: PropTypes.string.isRequired, + icons: PropTypes.shape({ + colored: PropTypes.elementType.isRequired, + hover: PropTypes.elementType.isRequired, + down: PropTypes.elementType.isRequired, + noFocus: PropTypes.elementType.isRequired + }).isRequired, + isGroupHovered: PropTypes.bool.isRequired, + activeAction: PropTypes.string, + isEnabled: PropTypes.bool.isRequired, + isWindowFocused: PropTypes.bool.isRequired, + onActive: PropTypes.func.isRequired, + onAction: PropTypes.func.isRequired +} + +const MacOSTrafficLights = () => { + const { handleWindowControl, isFullScreen } = useContext(ElectronContext) + const [isGroupHovered, setIsGroupHovered] = useState(false) + const [activeAction, setActiveAction] = useState(null) + const [isWindowFocused, setIsWindowFocused] = useState( + () => document.hasFocus?.() ?? true + ) + + useEffect(() => { + const handleFocus = () => setIsWindowFocused(true) + const handleBlur = () => setIsWindowFocused(false) + + window.addEventListener('focus', handleFocus) + window.addEventListener('blur', handleBlur) + + return () => { + window.removeEventListener('focus', handleFocus) + window.removeEventListener('blur', handleBlur) + } + }, []) + + const fullscreenIcons = isFullScreen + ? { + colored: FullscreenColored, + hover: ExitFullscreenHoverColored, + down: ExitFullscreenDownColored, + noFocus: NoFocus + } + : { + colored: FullscreenColored, + hover: FullscreenHoverColored, + down: FullscreenDownColored, + noFocus: NoFocus + } + + const handleAction = (action) => { + handleWindowControl(action) + } + + return ( + setIsGroupHovered(true)} + onMouseLeave={() => { + setIsGroupHovered(false) + setActiveAction(null) + }} + > + + + + + ) +} + +export default MacOSTrafficLights diff --git a/src/desktop/window.js b/src/desktop/window.js index 119dd5b..d4be591 100644 --- a/src/desktop/window.js +++ b/src/desktop/window.js @@ -93,6 +93,7 @@ function setupWindowEvents(window) { // Electrobun emits resize/focus, not Electron's maximize/fullscreen events. window.on?.('resize', broadcastWindowState) window.on?.('focus', broadcastWindowState) + window.on?.('move', broadcastWindowState) } export function setupMainWindowMessaging(window = mainWindow) { @@ -229,6 +230,14 @@ export function handleWindowControl(action) { mainWindow.maximize?.() } break + case 'fullscreen': + if (mainWindow.isFullScreen?.()) { + mainWindow.setFullScreen?.(false) + } else { + mainWindow.setFullScreen?.(true) + } + setTimeout(broadcastWindowState, 100) + break case 'close': mainWindow.close?.() break