Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
471 changes: 456 additions & 15 deletions apps/desktop/src/main.ts

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import type { DesktopBridge } from "@t3tools/contracts";
const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
const CONFIRM_CHANNEL = "desktop:confirm";
const SET_THEME_CHANNEL = "desktop:set-theme";
const SET_TITLE_BAR_MODE_CHANNEL = "desktop:set-title-bar-mode";
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
const WINDOW_STATE_CHANNEL = "desktop:window-state";
const WINDOW_GET_STATE_CHANNEL = "desktop:window-get-state";
const WINDOW_ACTION_CHANNEL = "desktop:window-action";
const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
Expand All @@ -21,6 +25,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL),
confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),
setTitleBarMode: (mode) => ipcRenderer.invoke(SET_TITLE_BAR_MODE_CHANNEL, mode),
showContextMenu: (items, position) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items, position),
openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url),
onMenuAction: (listener) => {
Expand All @@ -34,6 +39,19 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.removeListener(MENU_ACTION_CHANNEL, wrappedListener);
};
},
getWindowState: () => ipcRenderer.invoke(WINDOW_GET_STATE_CHANNEL),
performWindowAction: (action) => ipcRenderer.invoke(WINDOW_ACTION_CHANNEL, action),
onWindowState: (listener) => {
const wrappedListener = (_event: Electron.IpcRendererEvent, state: unknown) => {
if (typeof state !== "object" || state === null) return;
listener(state as Parameters<typeof listener>[0]);
};

ipcRenderer.on(WINDOW_STATE_CHANNEL, wrappedListener);
return () => {
ipcRenderer.removeListener(WINDOW_STATE_CHANNEL, wrappedListener);
};
},
getUpdateState: () => ipcRenderer.invoke(UPDATE_GET_STATE_CHANNEL),
downloadUpdate: () => ipcRenderer.invoke(UPDATE_DOWNLOAD_CHANNEL),
installUpdate: () => ipcRenderer.invoke(UPDATE_INSTALL_CHANNEL),
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";
import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";
import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
import { useShouldUseT3CodeWindowDecoration } from "~/hooks/useWindowDecorationMode";
import { isElectron } from "../env";
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
import {
Expand Down Expand Up @@ -241,6 +242,7 @@ interface ChatViewProps {
}

export default function ChatView({ threadId }: ChatViewProps) {
const shouldUseT3CodeWindowDecoration = useShouldUseT3CodeWindowDecoration();
const threads = useStore((store) => store.threads);
const projects = useStore((store) => store.projects);
const markThreadVisited = useStore((store) => store.markThreadVisited);
Expand Down Expand Up @@ -3476,7 +3478,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
</div>
</header>
)}
{isElectron && (
{shouldUseT3CodeWindowDecoration && (
<div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5">
<span className="text-xs text-muted-foreground/50">No active thread</span>
</div>
Expand All @@ -3496,7 +3498,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
<header
className={cn(
"border-b border-border px-3 sm:px-5",
isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
shouldUseT3CodeWindowDecoration
? "drag-region flex h-[52px] items-center"
: "py-2 sm:py-3",
)}
>
<ChatHeader
Expand Down
55 changes: 55 additions & 0 deletions apps/web/src/components/DesktopWindowTopBar.logic.ts
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;
}
258 changes: 258 additions & 0 deletions apps/web/src/components/DesktopWindowTopBar.tsx
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 };
Loading