Skip to content

Commit 9ef05fa

Browse files
committed
macos fixes for vtabbar / tabbar
1 parent f895fd9 commit 9ef05fa

8 files changed

Lines changed: 248 additions & 63 deletions

File tree

frontend/app/tab/tabcontent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const tileGapSizeAtom = atom((get) => {
1717
return settings["window:tilegapsize"];
1818
});
1919

20-
const TabContent = React.memo(({ tabId }: { tabId: string }) => {
20+
const TabContent = React.memo(({ tabId, noTopPadding }: { tabId: string; noTopPadding?: boolean }) => {
2121
const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]);
2222
const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]);
2323
const tabLoading = useAtomValue(loadingAtom);
@@ -67,7 +67,7 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
6767
}
6868

6969
return (
70-
<div className="flex flex-row flex-grow min-h-0 w-full items-center justify-center overflow-hidden relative pt-[3px] pr-[3px]">
70+
<div className={`flex flex-row flex-grow min-h-0 w-full items-center justify-center overflow-hidden relative ${noTopPadding ? "" : "pt-[3px]"} pr-[3px]`}>
7171
{innerContent}
7272
</div>
7373
);

frontend/app/tab/tabcontextmenu.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,26 @@ const FlagColors: { label: string; value: string }[] = [
1717
{ label: "Yellow", value: "#FFE900" },
1818
];
1919

20-
function buildTabContextMenu(
20+
export function buildTabBarContextMenu(env: TabEnv): ContextMenuItem[] {
21+
const currentTabBar = globalStore.get(env.getSettingsKeyAtom("app:tabbar")) ?? "top";
22+
const tabBarSubmenu: ContextMenuItem[] = [
23+
{
24+
label: "Top",
25+
type: "checkbox",
26+
checked: currentTabBar === "top",
27+
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "top" })),
28+
},
29+
{
30+
label: "Left",
31+
type: "checkbox",
32+
checked: currentTabBar === "left",
33+
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "left" })),
34+
},
35+
];
36+
return [{ label: "Tab Bar Position", type: "submenu", submenu: tabBarSubmenu }];
37+
}
38+
39+
export function buildTabContextMenu(
2140
id: string,
2241
renameRef: React.RefObject<(() => void) | null>,
2342
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void,
@@ -85,24 +104,7 @@ function buildTabContextMenu(
85104
}
86105
menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
87106
}
88-
const currentTabBar = globalStore.get(env.getSettingsKeyAtom("app:tabbar")) ?? "top";
89-
const tabBarSubmenu: ContextMenuItem[] = [
90-
{
91-
label: "Top",
92-
type: "checkbox",
93-
checked: currentTabBar === "top",
94-
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "top" })),
95-
},
96-
{
97-
label: "Left",
98-
type: "checkbox",
99-
checked: currentTabBar === "left",
100-
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "left" })),
101-
},
102-
];
103-
menu.push({ label: "Tab Bar Position", type: "submenu", submenu: tabBarSubmenu }, { type: "separator" });
107+
menu.push(...buildTabBarContextMenu(env), { type: "separator" });
104108
menu.push({ label: "Close Tab", click: () => onClose(null) });
105109
return menu;
106110
}
107-
108-
export { buildTabContextMenu };

frontend/app/tab/updatebanner.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { Tooltip } from "@/element/tooltip";
5-
import { useWaveEnv } from "@/app/waveenv/waveenv";
6-
import { TabBarEnv } from "./tabbarenv";
5+
import { WaveEnv, WaveEnvSubset, useWaveEnv } from "@/app/waveenv/waveenv";
76
import { useAtomValue } from "jotai";
87
import { memo, useCallback } from "react";
98

9+
type UpdateBannerEnv = WaveEnvSubset<{
10+
electron: {
11+
installAppUpdate: WaveEnv["electron"]["installAppUpdate"];
12+
};
13+
atoms: {
14+
updaterStatusAtom: WaveEnv["atoms"]["updaterStatusAtom"];
15+
};
16+
}>;
17+
1018
function getUpdateStatusMessage(status: string): string {
1119
switch (status) {
1220
case "ready":
@@ -21,7 +29,7 @@ function getUpdateStatusMessage(status: string): string {
2129
}
2230

2331
const UpdateStatusBannerComponent = () => {
24-
const env = useWaveEnv<TabBarEnv>();
32+
const env = useWaveEnv<UpdateBannerEnv>();
2533
const appUpdateStatus = useAtomValue(env.atoms.updaterStatusAtom);
2634
const updateStatusMessage = getUpdateStatusMessage(appUpdateStatus);
2735

frontend/app/tab/vtabbar.tsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,83 @@
11
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { Tooltip } from "@/app/element/tooltip";
45
import { getTabBadgeAtom } from "@/app/store/badge";
56
import { makeORef } from "@/app/store/wos";
67
import { TabRpcClient } from "@/app/store/wshrpcutil";
78
import { useWaveEnv } from "@/app/waveenv/waveenv";
9+
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
810
import { validateCssColor } from "@/util/color-validator";
911
import { cn, fireAndForget } from "@/util/util";
1012
import { useAtomValue } from "jotai";
11-
import { useCallback, useEffect, useRef, useState } from "react";
12-
import { buildTabContextMenu } from "./tabcontextmenu";
13+
import { memo, useCallback, useEffect, useRef, useState } from "react";
14+
import { buildTabBarContextMenu, buildTabContextMenu } from "./tabcontextmenu";
15+
import { UpdateStatusBanner } from "./updatebanner";
1316
import { VTab, VTabItem } from "./vtab";
1417
import { VTabBarEnv } from "./vtabbarenv";
18+
import { WorkspaceSwitcher } from "./workspaceswitcher";
1519
export type { VTabItem } from "./vtab";
1620

21+
const VTabBarAIButton = memo(() => {
22+
const env = useWaveEnv<VTabBarEnv>();
23+
const aiPanelOpen = useAtomValue(WorkspaceLayoutModel.getInstance().panelVisibleAtom);
24+
const hideAiButton = useAtomValue(env.getSettingsKeyAtom("app:hideaibutton"));
25+
26+
const onClick = () => {
27+
const currentVisible = WorkspaceLayoutModel.getInstance().getAIPanelVisible();
28+
WorkspaceLayoutModel.getInstance().setAIPanelVisible(!currentVisible);
29+
};
30+
31+
if (hideAiButton) {
32+
return null;
33+
}
34+
35+
return (
36+
<Tooltip
37+
content="Toggle Wave AI Panel"
38+
placement="bottom"
39+
hideOnClick
40+
divClassName={`flex h-[22px] px-3.5 justify-end mb-1 items-center rounded-md mr-1 box-border cursor-pointer bg-hover hover:bg-hoverbg transition-colors text-[12px] ${aiPanelOpen ? "text-accent" : "text-secondary"}`}
41+
divStyle={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
42+
divOnClick={onClick}
43+
>
44+
<i className="fa fa-sparkles" />
45+
</Tooltip>
46+
);
47+
});
48+
VTabBarAIButton.displayName = "VTabBarAIButton";
49+
50+
const MacOSHeader = memo(() => {
51+
const env = useWaveEnv<VTabBarEnv>();
52+
const isFullScreen = useAtomValue(env.atoms.isFullScreen);
53+
return (
54+
<>
55+
{!isFullScreen && (
56+
<div
57+
className="w-full shrink-0"
58+
style={
59+
{
60+
height: "calc(25px * var(--zoomfactor-inv))",
61+
WebkitAppRegion: "drag",
62+
} as React.CSSProperties
63+
}
64+
/>
65+
)}
66+
<div
67+
className="flex shrink-0 flex-row flex-wrap items-end px-1 pb-1 pl-2"
68+
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
69+
>
70+
<VTabBarAIButton />
71+
<Tooltip content="Workspace Switcher" placement="bottom" hideOnClick divClassName="flex items-center">
72+
<WorkspaceSwitcher />
73+
</Tooltip>
74+
<UpdateStatusBanner />
75+
</div>
76+
</>
77+
);
78+
});
79+
MacOSHeader.displayName = "MacOSHeader";
80+
1781
interface VTabBarProps {
1882
workspace: Workspace;
1983
className?: string;
@@ -79,6 +143,7 @@ function VTabWrapper({
79143
const handleContextMenu = useCallback(
80144
(e: React.MouseEvent<HTMLDivElement>) => {
81145
e.preventDefault();
146+
e.stopPropagation();
82147
const menu = buildTabContextMenu(tabId, renameRef, () => onClose(), env);
83148
env.showContextMenu(menu, e);
84149
},
@@ -238,11 +303,22 @@ export function VTabBar({ workspace, className }: VTabBarProps) {
238303
fireAndForget(() => env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, nextTabIds));
239304
};
240305

306+
const handleTabBarContextMenu = useCallback(
307+
(e: React.MouseEvent<HTMLDivElement>) => {
308+
e.preventDefault();
309+
const menu = buildTabBarContextMenu(env);
310+
env.showContextMenu(menu, e);
311+
},
312+
[env]
313+
);
314+
241315
return (
242316
<div
243317
className={cn("flex h-full flex-col overflow-hidden", className)}
244318
style={{ backdropFilter: "blur(20px)", background: "rgba(0, 0, 0, 0.35)" }}
319+
onContextMenu={handleTabBarContextMenu}
245320
>
321+
{env.isMacOS() && <MacOSHeader />}
246322
<div
247323
ref={scrollContainerRef}
248324
className="relative flex min-h-0 flex-col overflow-y-auto"

frontend/app/tab/vtabbarenv.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export type VTabBarEnv = WaveEnvSubset<{
88
createTab: WaveEnv["electron"]["createTab"];
99
closeTab: WaveEnv["electron"]["closeTab"];
1010
setActiveTab: WaveEnv["electron"]["setActiveTab"];
11+
deleteWorkspace: WaveEnv["electron"]["deleteWorkspace"];
12+
createWorkspace: WaveEnv["electron"]["createWorkspace"];
13+
switchWorkspace: WaveEnv["electron"]["switchWorkspace"];
14+
installAppUpdate: WaveEnv["electron"]["installAppUpdate"];
1115
};
1216
rpc: {
1317
UpdateWorkspaceTabIdsCommand: WaveEnv["rpc"]["UpdateWorkspaceTabIdsCommand"];
@@ -21,10 +25,16 @@ export type VTabBarEnv = WaveEnvSubset<{
2125
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
2226
reinitVersion: WaveEnv["atoms"]["reinitVersion"];
2327
documentHasFocus: WaveEnv["atoms"]["documentHasFocus"];
28+
workspace: WaveEnv["atoms"]["workspace"];
29+
updaterStatusAtom: WaveEnv["atoms"]["updaterStatusAtom"];
30+
isFullScreen: WaveEnv["atoms"]["isFullScreen"];
31+
};
32+
services: {
33+
workspace: WaveEnv["services"]["workspace"];
2434
};
2535
wos: WaveEnv["wos"];
2636
showContextMenu: WaveEnv["showContextMenu"];
27-
getSettingsKeyAtom: SettingsKeyAtomFnType<"tab:confirmclose" | "app:tabbar">;
37+
getSettingsKeyAtom: SettingsKeyAtomFnType<"tab:confirmclose" | "app:tabbar" | "app:hideaibutton">;
2838
mockSetWaveObj: WaveEnv["mockSetWaveObj"];
2939
isWindows: WaveEnv["isWindows"];
3040
isMacOS: WaveEnv["isMacOS"];

frontend/app/workspace/workspace-layout-model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const AIPanel_MinWidth = 300;
2222
const AIPanel_MaxWidthRatio = 0.66;
2323

2424
const VTabBar_DefaultWidth = 220;
25-
const VTabBar_MinWidth = 100;
25+
const VTabBar_MinWidth = 110;
2626
const VTabBar_MaxWidth = 280;
2727

2828
function clampVTabWidth(w: number): number {

frontend/app/workspace/workspace.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { VTabBar } from "@/app/tab/vtabbar";
1111
import { Widgets } from "@/app/workspace/widgets";
1212
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
1313
import { atoms, getApi, getSettingsKeyAtom } from "@/store/global";
14+
import { isMacOS } from "@/util/platformutil";
1415
import { useAtomValue } from "jotai";
1516
import { memo, useEffect, useRef } from "react";
1617
import {
@@ -21,6 +22,23 @@ import {
2122
PanelResizeHandle,
2223
} from "react-resizable-panels";
2324

25+
const MacOSTabBarSpacer = memo(() => {
26+
return (
27+
<div
28+
className="w-full shrink-0"
29+
style={
30+
{
31+
height: "calc(8px * var(--zoomfactor-inv))",
32+
WebkitAppRegion: "drag",
33+
backdropFilter: "blur(20px)",
34+
background: "rgba(0, 0, 0, 0.35)",
35+
} as React.CSSProperties
36+
}
37+
/>
38+
);
39+
});
40+
MacOSTabBarSpacer.displayName = "MacOSTabBarSpacer";
41+
2442
const WorkspaceElem = memo(() => {
2543
const workspaceLayoutModel = WorkspaceLayoutModel.getInstance();
2644
const tabId = useAtomValue(atoms.staticTabId);
@@ -87,19 +105,16 @@ const WorkspaceElem = memo(() => {
87105

88106
return (
89107
<div className="flex flex-col w-full flex-grow overflow-hidden">
90-
<TabBar key={ws.oid} workspace={ws} noTabs={showLeftTabBar} />
108+
{!(showLeftTabBar && isMacOS()) && <TabBar key={ws.oid} workspace={ws} noTabs={showLeftTabBar} />}
109+
{showLeftTabBar && isMacOS() && <MacOSTabBarSpacer />}
91110
<div ref={panelContainerRef} className="flex flex-row flex-grow overflow-hidden">
92111
<ErrorBoundary key={tabId}>
93112
<PanelGroup
94113
direction="horizontal"
95114
onLayout={workspaceLayoutModel.handleOuterPanelLayout}
96115
ref={outerPanelGroupRef}
97116
>
98-
<Panel
99-
order={0}
100-
defaultSize={leftGroupInitialPct}
101-
className="overflow-hidden"
102-
>
117+
<Panel order={0} defaultSize={leftGroupInitialPct} className="overflow-hidden">
103118
<PanelGroup
104119
direction="horizontal"
105120
onLayout={workspaceLayoutModel.handleInnerPanelLayout}
@@ -122,7 +137,10 @@ const WorkspaceElem = memo(() => {
122137
order={1}
123138
className="overflow-hidden"
124139
>
125-
<div ref={aiPanelWrapperRef} className={`w-full h-full pr-0.5 ${aiPanelVisible ? "" : "opacity-0"}`}>
140+
<div
141+
ref={aiPanelWrapperRef}
142+
className={`w-full h-full pr-0.5 ${aiPanelVisible ? "" : "opacity-0"}`}
143+
>
126144
{tabId !== "" && <AIPanel roundTopLeft={showLeftTabBar} />}
127145
</div>
128146
</Panel>
@@ -134,7 +152,7 @@ const WorkspaceElem = memo(() => {
134152
<CenteredDiv>No Active Tab</CenteredDiv>
135153
) : (
136154
<div className="flex flex-row h-full">
137-
<TabContent key={tabId} tabId={tabId} />
155+
<TabContent key={tabId} tabId={tabId} noTopPadding={showLeftTabBar && isMacOS()} />
138156
<Widgets />
139157
</div>
140158
)}

0 commit comments

Comments
 (0)