-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add desktop window decoration modes #1449
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
Open
smalitobules
wants to merge
15
commits into
pingdotgg:main
Choose a base branch
from
smalitobules:feat/window-decoration-fullscreen-topbar
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,166
−69
Open
Changes from 1 commit
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
ca8eefa
Add desktop window decoration modes
smalitobules bd6c0d0
Harden desktop window decoration state handling
smalitobules 3c6b557
Handle macOS system title bar inset
smalitobules e7eb35e
fix: tolerate corrupt desktop settings
smalitobules 3dee4d9
fix: handle title bar reset failures
smalitobules 99b331e
Harden desktop title bar mode transitions
smalitobules d94f2dc
Limit sidebar inset to macOS system chrome
smalitobules 2912f04
Hide top bar when entering full screen
smalitobules c9e93cf
Reuse desktop header drag region state
smalitobules a0bce45
Move title bar mode to desktop settings
smalitobules b2f083e
Restore main window after failed rebuild
smalitobules d85a533
Reuse title bar mode apply path
smalitobules 98230aa
Close rebuilt window after IPC response
smalitobules 4ff6c86
Reset rebuild state on sync failure
smalitobules 257467c
Queue title bar mode changes during rebuild
smalitobules File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { DesktopWindowState } from "@t3tools/contracts"; | ||
|
|
||
| export const DESKTOP_WINDOW_TOP_BAR_HEIGHT_PX = 44; | ||
| export const DESKTOP_WINDOW_TOP_BAR_REVEAL_ZONE_PX = 4; | ||
| export const DESKTOP_WINDOW_TOP_BAR_MARGIN_X_PX = 8; | ||
| export const DESKTOP_WINDOW_TOP_BAR_MARGIN_TOP_PX = 8; | ||
|
|
||
| export function resolveDesktopWindowTopBarZoomFactor( | ||
| windowState: DesktopWindowState | null, | ||
| ): number { | ||
| const zoomFactor = windowState?.zoomFactor ?? 1; | ||
| return Number.isFinite(zoomFactor) && zoomFactor > 0 ? zoomFactor : 1; | ||
| } | ||
|
|
||
| export function shouldUseDesktopWindowTopBar(windowState: DesktopWindowState | null): boolean { | ||
| return ( | ||
| windowState !== null && | ||
| windowState.platform !== "other" && | ||
| windowState.titleBarMode === "t3code" | ||
| ); | ||
| } | ||
|
|
||
| export function shouldOverlayDesktopWindowTopBar(windowState: DesktopWindowState | null): boolean { | ||
| return shouldUseDesktopWindowTopBar(windowState) && windowState?.isFullScreen === true; | ||
| } | ||
|
|
||
| export function nextDesktopWindowTopBarVisibility(input: { | ||
| windowState: DesktopWindowState | null; | ||
| pointerY: number | null; | ||
| isHovered: boolean; | ||
| wasVisible: boolean; | ||
| }): boolean { | ||
| if (!shouldUseDesktopWindowTopBar(input.windowState)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (input.windowState?.isFullScreen !== true) { | ||
| return true; | ||
| } | ||
|
|
||
| if (input.isHovered) { | ||
| return true; | ||
| } | ||
|
|
||
| if (input.pointerY === null) { | ||
| return false; | ||
| } | ||
|
|
||
| const zoomFactor = resolveDesktopWindowTopBarZoomFactor(input.windowState); | ||
| const revealThreshold = input.wasVisible | ||
| ? DESKTOP_WINDOW_TOP_BAR_HEIGHT_PX / zoomFactor | ||
| : DESKTOP_WINDOW_TOP_BAR_REVEAL_ZONE_PX / zoomFactor; | ||
|
|
||
| return input.pointerY <= revealThreshold; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,258 @@ | ||
| import type { DesktopWindowAction, DesktopWindowState } from "@t3tools/contracts"; | ||
| import { Maximize2Icon, Minimize2Icon, MinusIcon, XIcon } from "lucide-react"; | ||
| import { useEffect, useEffectEvent, useState } from "react"; | ||
|
|
||
| import { APP_DISPLAY_NAME } from "~/branding"; | ||
| import { | ||
| DESKTOP_WINDOW_TOP_BAR_HEIGHT_PX, | ||
| DESKTOP_WINDOW_TOP_BAR_MARGIN_TOP_PX, | ||
| DESKTOP_WINDOW_TOP_BAR_MARGIN_X_PX, | ||
| nextDesktopWindowTopBarVisibility, | ||
| resolveDesktopWindowTopBarZoomFactor, | ||
| shouldOverlayDesktopWindowTopBar, | ||
| shouldUseDesktopWindowTopBar, | ||
| } from "~/components/DesktopWindowTopBar.logic"; | ||
| import { Button } from "~/components/ui/button"; | ||
| import { useDesktopWindowState } from "~/hooks/useDesktopWindowState"; | ||
| import { cn } from "~/lib/utils"; | ||
|
|
||
| function DesktopWindowTopBar() { | ||
| const windowState = useDesktopWindowState(); | ||
| const [isHovered, setIsHovered] = useState(false); | ||
| const [isVisible, setIsVisible] = useState(false); | ||
|
|
||
| const syncVisibility = useEffectEvent((nextState: DesktopWindowState | null) => { | ||
| if (!shouldUseDesktopWindowTopBar(nextState)) { | ||
| setIsHovered(false); | ||
| setIsVisible(false); | ||
| return; | ||
| } | ||
|
|
||
| if (nextState?.isFullScreen !== true) { | ||
| setIsHovered(false); | ||
| setIsVisible(true); | ||
| } | ||
| }); | ||
|
|
||
| const updateVisibility = useEffectEvent((pointerY: number | null) => { | ||
| setIsVisible((wasVisible) => | ||
| nextDesktopWindowTopBarVisibility({ | ||
| windowState, | ||
| pointerY, | ||
| isHovered, | ||
| wasVisible, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| const performWindowAction = useEffectEvent(async (action: DesktopWindowAction) => { | ||
| const bridge = window.desktopBridge; | ||
| if (!bridge) { | ||
| return; | ||
| } | ||
|
|
||
| await bridge.performWindowAction(action); | ||
| if (action === "minimize" || action === "close" || action === "exit-full-screen") { | ||
| setIsHovered(false); | ||
| setIsVisible(false); | ||
| } | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| syncVisibility(windowState); | ||
| }, [windowState]); | ||
|
|
||
| useEffect(() => { | ||
| if (!shouldOverlayDesktopWindowTopBar(windowState)) { | ||
| return; | ||
| } | ||
|
|
||
| const handlePointerMove = (event: MouseEvent) => { | ||
| updateVisibility(event.clientY); | ||
| }; | ||
| const handlePointerLeave = () => { | ||
| setIsHovered(false); | ||
| setIsVisible(false); | ||
| }; | ||
|
|
||
| window.addEventListener("mousemove", handlePointerMove); | ||
| window.addEventListener("mouseleave", handlePointerLeave); | ||
|
|
||
| return () => { | ||
| window.removeEventListener("mousemove", handlePointerMove); | ||
| window.removeEventListener("mouseleave", handlePointerLeave); | ||
| }; | ||
| }, [windowState]); | ||
|
|
||
| if (!windowState || !shouldUseDesktopWindowTopBar(windowState)) { | ||
| return null; | ||
| } | ||
|
|
||
| const activeWindowState = windowState; | ||
| const isMacWindow = activeWindowState.platform === "darwin"; | ||
| const isOverlay = shouldOverlayDesktopWindowTopBar(activeWindowState); | ||
| const zoomFactor = resolveDesktopWindowTopBarZoomFactor(activeWindowState); | ||
| const inverseZoomFactor = 1 / zoomFactor; | ||
| const frameHeightPx = | ||
| (DESKTOP_WINDOW_TOP_BAR_HEIGHT_PX + (isOverlay ? DESKTOP_WINDOW_TOP_BAR_MARGIN_TOP_PX : 0)) / | ||
| zoomFactor; | ||
| const frameWidth = isOverlay | ||
| ? `calc(${100 * zoomFactor}% - ${2 * DESKTOP_WINDOW_TOP_BAR_MARGIN_X_PX}px)` | ||
| : `${100 * zoomFactor}%`; | ||
| const secondaryAction: { | ||
| action: DesktopWindowAction; | ||
| label: string; | ||
| icon: typeof Maximize2Icon; | ||
| } = activeWindowState.isFullScreen | ||
| ? { | ||
| action: "exit-full-screen", | ||
| label: "Exit full screen", | ||
| icon: Minimize2Icon, | ||
| } | ||
| : activeWindowState.isMaximized | ||
| ? { | ||
| action: "toggle-maximize", | ||
| label: "Restore window", | ||
| icon: Minimize2Icon, | ||
| } | ||
| : { | ||
| action: "toggle-maximize", | ||
| label: "Maximize window", | ||
| icon: Maximize2Icon, | ||
| }; | ||
|
|
||
| const SecondaryActionIcon = secondaryAction.icon; | ||
| const windowActions: ReadonlyArray<{ | ||
| action: DesktopWindowAction; | ||
| icon: typeof Maximize2Icon; | ||
| label: string; | ||
| }> = isMacWindow | ||
| ? [ | ||
| { | ||
| action: "close", | ||
| icon: XIcon, | ||
| label: "Close window", | ||
| }, | ||
| { | ||
| action: "minimize", | ||
| icon: MinusIcon, | ||
| label: "Minimize window", | ||
| }, | ||
| { | ||
| action: secondaryAction.action, | ||
| icon: SecondaryActionIcon, | ||
| label: secondaryAction.label, | ||
| }, | ||
| ] | ||
| : [ | ||
| { | ||
| action: "minimize", | ||
| icon: MinusIcon, | ||
| label: "Minimize window", | ||
| }, | ||
| { | ||
| action: secondaryAction.action, | ||
| icon: SecondaryActionIcon, | ||
| label: secondaryAction.label, | ||
| }, | ||
| { | ||
| action: "close", | ||
| icon: XIcon, | ||
| label: "Close window", | ||
| }, | ||
| ]; | ||
|
|
||
| const content = ( | ||
| <div className="relative w-full shrink-0" style={{ height: `${frameHeightPx}px` }}> | ||
| <div | ||
| className={cn( | ||
| "drag-region absolute left-0 top-0 flex items-center border-border/80 bg-background/92 backdrop-blur-md", | ||
| isMacWindow ? "justify-start" : "justify-end", | ||
| isOverlay | ||
| ? "pointer-events-auto mx-2 mt-2 rounded-xl border px-2 shadow-lg" | ||
| : "border-b px-3 shadow-[0_1px_0_rgba(255,255,255,0.04)]", | ||
| )} | ||
| onDoubleClick={() => { | ||
| if (!activeWindowState.isFullScreen) { | ||
| void performWindowAction("toggle-maximize"); | ||
| } | ||
| }} | ||
| onMouseEnter={() => { | ||
| if (isOverlay) { | ||
| setIsHovered(true); | ||
| setIsVisible(true); | ||
| } | ||
| }} | ||
| onMouseLeave={() => { | ||
| if (isOverlay) { | ||
| setIsHovered(false); | ||
| setIsVisible(false); | ||
| } | ||
| }} | ||
| onFocusCapture={() => { | ||
| if (isOverlay) { | ||
| setIsHovered(true); | ||
| setIsVisible(true); | ||
| } | ||
| }} | ||
| onBlurCapture={(event) => { | ||
| if (!isOverlay) { | ||
| return; | ||
| } | ||
|
|
||
| const relatedTarget = event.relatedTarget; | ||
| if (!(relatedTarget instanceof Node) || !event.currentTarget.contains(relatedTarget)) { | ||
| setIsHovered(false); | ||
| setIsVisible(false); | ||
| } | ||
| }} | ||
| style={{ | ||
| height: `${DESKTOP_WINDOW_TOP_BAR_HEIGHT_PX}px`, | ||
| transform: `scale(${inverseZoomFactor})`, | ||
| transformOrigin: "top left", | ||
| width: frameWidth, | ||
| }} | ||
| > | ||
| <div className="pointer-events-none absolute inset-0 flex items-center justify-center px-24 text-sm font-medium text-foreground/80"> | ||
| <span className="truncate">{APP_DISPLAY_NAME}</span> | ||
| </div> | ||
|
|
||
| <div className="relative z-10 flex items-center gap-1"> | ||
| {windowActions.map((windowAction) => { | ||
| const ActionIcon = windowAction.icon; | ||
| return ( | ||
| <Button | ||
| key={`${windowAction.action}-${windowAction.label}`} | ||
| aria-label={windowAction.label} | ||
| size="icon-xs" | ||
| variant="ghost" | ||
| onClick={() => { | ||
| void performWindowAction(windowAction.action); | ||
| }} | ||
| > | ||
| <ActionIcon /> | ||
| </Button> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| if (!isOverlay) { | ||
| return content; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| "pointer-events-none fixed inset-x-0 top-0 z-[90] transition-all duration-150", | ||
| isVisible ? "translate-y-0 opacity-100" : "-translate-y-full opacity-0", | ||
| )} | ||
| > | ||
| {content} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export { DesktopWindowTopBar }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.