Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
macos fixes for vtabbar / tabbar
  • Loading branch information
sawka committed Mar 14, 2026
commit 9ef05fa3a06a271b19d759587287697ce3a1b3f1
4 changes: 2 additions & 2 deletions frontend/app/tab/tabcontent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const tileGapSizeAtom = atom((get) => {
return settings["window:tilegapsize"];
});

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

return (
<div className="flex flex-row flex-grow min-h-0 w-full items-center justify-center overflow-hidden relative pt-[3px] pr-[3px]">
<div className={`flex flex-row flex-grow min-h-0 w-full items-center justify-center overflow-hidden relative ${noTopPadding ? "" : "pt-[3px]"} pr-[3px]`}>
{innerContent}
</div>
);
Expand Down
40 changes: 21 additions & 19 deletions frontend/app/tab/tabcontextmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,26 @@ const FlagColors: { label: string; value: string }[] = [
{ label: "Yellow", value: "#FFE900" },
];

function buildTabContextMenu(
export function buildTabBarContextMenu(env: TabEnv): ContextMenuItem[] {
const currentTabBar = globalStore.get(env.getSettingsKeyAtom("app:tabbar")) ?? "top";
const tabBarSubmenu: ContextMenuItem[] = [
{
label: "Top",
type: "checkbox",
checked: currentTabBar === "top",
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "top" })),
},
{
label: "Left",
type: "checkbox",
checked: currentTabBar === "left",
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "left" })),
},
];
return [{ label: "Tab Bar Position", type: "submenu", submenu: tabBarSubmenu }];
}

export function buildTabContextMenu(
id: string,
renameRef: React.RefObject<(() => void) | null>,
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void,
Expand Down Expand Up @@ -85,24 +104,7 @@ function buildTabContextMenu(
}
menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
}
const currentTabBar = globalStore.get(env.getSettingsKeyAtom("app:tabbar")) ?? "top";
const tabBarSubmenu: ContextMenuItem[] = [
{
label: "Top",
type: "checkbox",
checked: currentTabBar === "top",
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "top" })),
},
{
label: "Left",
type: "checkbox",
checked: currentTabBar === "left",
click: () => fireAndForget(() => env.rpc.SetConfigCommand(TabRpcClient, { "app:tabbar": "left" })),
},
];
menu.push({ label: "Tab Bar Position", type: "submenu", submenu: tabBarSubmenu }, { type: "separator" });
menu.push(...buildTabBarContextMenu(env), { type: "separator" });
menu.push({ label: "Close Tab", click: () => onClose(null) });
return menu;
}

export { buildTabContextMenu };
14 changes: 11 additions & 3 deletions frontend/app/tab/updatebanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
// SPDX-License-Identifier: Apache-2.0

import { Tooltip } from "@/element/tooltip";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { TabBarEnv } from "./tabbarenv";
import { WaveEnv, WaveEnvSubset, useWaveEnv } from "@/app/waveenv/waveenv";
import { useAtomValue } from "jotai";
import { memo, useCallback } from "react";

type UpdateBannerEnv = WaveEnvSubset<{
electron: {
installAppUpdate: WaveEnv["electron"]["installAppUpdate"];
};
atoms: {
updaterStatusAtom: WaveEnv["atoms"]["updaterStatusAtom"];
};
}>;

function getUpdateStatusMessage(status: string): string {
switch (status) {
case "ready":
Expand All @@ -21,7 +29,7 @@ function getUpdateStatusMessage(status: string): string {
}

const UpdateStatusBannerComponent = () => {
const env = useWaveEnv<TabBarEnv>();
const env = useWaveEnv<UpdateBannerEnv>();
const appUpdateStatus = useAtomValue(env.atoms.updaterStatusAtom);
const updateStatusMessage = getUpdateStatusMessage(appUpdateStatus);

Expand Down
80 changes: 78 additions & 2 deletions frontend/app/tab/vtabbar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,83 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { Tooltip } from "@/app/element/tooltip";
import { getTabBadgeAtom } from "@/app/store/badge";
import { makeORef } from "@/app/store/wos";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import { validateCssColor } from "@/util/color-validator";
import { cn, fireAndForget } from "@/util/util";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useRef, useState } from "react";
import { buildTabContextMenu } from "./tabcontextmenu";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { buildTabBarContextMenu, buildTabContextMenu } from "./tabcontextmenu";
import { UpdateStatusBanner } from "./updatebanner";
import { VTab, VTabItem } from "./vtab";
import { VTabBarEnv } from "./vtabbarenv";
import { WorkspaceSwitcher } from "./workspaceswitcher";
export type { VTabItem } from "./vtab";

const VTabBarAIButton = memo(() => {
const env = useWaveEnv<VTabBarEnv>();
const aiPanelOpen = useAtomValue(WorkspaceLayoutModel.getInstance().panelVisibleAtom);
const hideAiButton = useAtomValue(env.getSettingsKeyAtom("app:hideaibutton"));

const onClick = () => {
const currentVisible = WorkspaceLayoutModel.getInstance().getAIPanelVisible();
WorkspaceLayoutModel.getInstance().setAIPanelVisible(!currentVisible);
};

if (hideAiButton) {
return null;
}

return (
<Tooltip
content="Toggle Wave AI Panel"
placement="bottom"
hideOnClick
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"}`}
divStyle={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
divOnClick={onClick}
>
<i className="fa fa-sparkles" />
</Tooltip>
);
});
VTabBarAIButton.displayName = "VTabBarAIButton";

const MacOSHeader = memo(() => {
const env = useWaveEnv<VTabBarEnv>();
const isFullScreen = useAtomValue(env.atoms.isFullScreen);
return (
<>
{!isFullScreen && (
<div
className="w-full shrink-0"
style={
{
height: "calc(25px * var(--zoomfactor-inv))",
WebkitAppRegion: "drag",
} as React.CSSProperties
}
/>
)}
<div
className="flex shrink-0 flex-row flex-wrap items-end px-1 pb-1 pl-2"
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
<VTabBarAIButton />
<Tooltip content="Workspace Switcher" placement="bottom" hideOnClick divClassName="flex items-center">
<WorkspaceSwitcher />
</Tooltip>
<UpdateStatusBanner />
</div>
</>
);
});
MacOSHeader.displayName = "MacOSHeader";

interface VTabBarProps {
workspace: Workspace;
className?: string;
Expand Down Expand Up @@ -79,6 +143,7 @@ function VTabWrapper({
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const menu = buildTabContextMenu(tabId, renameRef, () => onClose(), env);
env.showContextMenu(menu, e);
},
Expand Down Expand Up @@ -238,11 +303,22 @@ export function VTabBar({ workspace, className }: VTabBarProps) {
fireAndForget(() => env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, nextTabIds));
};

const handleTabBarContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
const menu = buildTabBarContextMenu(env);
env.showContextMenu(menu, e);
},
[env]
);

return (
<div
className={cn("flex h-full flex-col overflow-hidden", className)}
style={{ backdropFilter: "blur(20px)", background: "rgba(0, 0, 0, 0.35)" }}
onContextMenu={handleTabBarContextMenu}
>
{env.isMacOS() && <MacOSHeader />}
<div
ref={scrollContainerRef}
className="relative flex min-h-0 flex-col overflow-y-auto"
Expand Down
12 changes: 11 additions & 1 deletion frontend/app/tab/vtabbarenv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export type VTabBarEnv = WaveEnvSubset<{
createTab: WaveEnv["electron"]["createTab"];
closeTab: WaveEnv["electron"]["closeTab"];
setActiveTab: WaveEnv["electron"]["setActiveTab"];
deleteWorkspace: WaveEnv["electron"]["deleteWorkspace"];
createWorkspace: WaveEnv["electron"]["createWorkspace"];
switchWorkspace: WaveEnv["electron"]["switchWorkspace"];
installAppUpdate: WaveEnv["electron"]["installAppUpdate"];
};
rpc: {
UpdateWorkspaceTabIdsCommand: WaveEnv["rpc"]["UpdateWorkspaceTabIdsCommand"];
Expand All @@ -21,10 +25,16 @@ export type VTabBarEnv = WaveEnvSubset<{
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
reinitVersion: WaveEnv["atoms"]["reinitVersion"];
documentHasFocus: WaveEnv["atoms"]["documentHasFocus"];
workspace: WaveEnv["atoms"]["workspace"];
updaterStatusAtom: WaveEnv["atoms"]["updaterStatusAtom"];
isFullScreen: WaveEnv["atoms"]["isFullScreen"];
};
services: {
workspace: WaveEnv["services"]["workspace"];
};
wos: WaveEnv["wos"];
showContextMenu: WaveEnv["showContextMenu"];
getSettingsKeyAtom: SettingsKeyAtomFnType<"tab:confirmclose" | "app:tabbar">;
getSettingsKeyAtom: SettingsKeyAtomFnType<"tab:confirmclose" | "app:tabbar" | "app:hideaibutton">;
mockSetWaveObj: WaveEnv["mockSetWaveObj"];
isWindows: WaveEnv["isWindows"];
isMacOS: WaveEnv["isMacOS"];
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/workspace/workspace-layout-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const AIPanel_MinWidth = 300;
const AIPanel_MaxWidthRatio = 0.66;

const VTabBar_DefaultWidth = 220;
const VTabBar_MinWidth = 100;
const VTabBar_MinWidth = 110;
const VTabBar_MaxWidth = 280;

function clampVTabWidth(w: number): number {
Expand Down
34 changes: 26 additions & 8 deletions frontend/app/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { VTabBar } from "@/app/tab/vtabbar";
import { Widgets } from "@/app/workspace/widgets";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import { atoms, getApi, getSettingsKeyAtom } from "@/store/global";
import { isMacOS } from "@/util/platformutil";
import { useAtomValue } from "jotai";
import { memo, useEffect, useRef } from "react";
import {
Expand All @@ -21,6 +22,23 @@ import {
PanelResizeHandle,
} from "react-resizable-panels";

const MacOSTabBarSpacer = memo(() => {
return (
<div
className="w-full shrink-0"
style={
{
height: "calc(8px * var(--zoomfactor-inv))",
WebkitAppRegion: "drag",
backdropFilter: "blur(20px)",
background: "rgba(0, 0, 0, 0.35)",
} as React.CSSProperties
}
/>
);
});
MacOSTabBarSpacer.displayName = "MacOSTabBarSpacer";

const WorkspaceElem = memo(() => {
const workspaceLayoutModel = WorkspaceLayoutModel.getInstance();
const tabId = useAtomValue(atoms.staticTabId);
Expand Down Expand Up @@ -87,19 +105,16 @@ const WorkspaceElem = memo(() => {

return (
<div className="flex flex-col w-full flex-grow overflow-hidden">
<TabBar key={ws.oid} workspace={ws} noTabs={showLeftTabBar} />
{!(showLeftTabBar && isMacOS()) && <TabBar key={ws.oid} workspace={ws} noTabs={showLeftTabBar} />}
{showLeftTabBar && isMacOS() && <MacOSTabBarSpacer />}
<div ref={panelContainerRef} className="flex flex-row flex-grow overflow-hidden">
<ErrorBoundary key={tabId}>
<PanelGroup
direction="horizontal"
onLayout={workspaceLayoutModel.handleOuterPanelLayout}
ref={outerPanelGroupRef}
>
<Panel
order={0}
defaultSize={leftGroupInitialPct}
className="overflow-hidden"
>
<Panel order={0} defaultSize={leftGroupInitialPct} className="overflow-hidden">
<PanelGroup
direction="horizontal"
onLayout={workspaceLayoutModel.handleInnerPanelLayout}
Expand All @@ -122,7 +137,10 @@ const WorkspaceElem = memo(() => {
order={1}
className="overflow-hidden"
>
<div ref={aiPanelWrapperRef} className={`w-full h-full pr-0.5 ${aiPanelVisible ? "" : "opacity-0"}`}>
<div
ref={aiPanelWrapperRef}
className={`w-full h-full pr-0.5 ${aiPanelVisible ? "" : "opacity-0"}`}
>
{tabId !== "" && <AIPanel roundTopLeft={showLeftTabBar} />}
</div>
</Panel>
Expand All @@ -134,7 +152,7 @@ const WorkspaceElem = memo(() => {
<CenteredDiv>No Active Tab</CenteredDiv>
) : (
<div className="flex flex-row h-full">
<TabContent key={tabId} tabId={tabId} />
<TabContent key={tabId} tabId={tabId} noTopPadding={showLeftTabBar && isMacOS()} />
<Widgets />
</div>
)}
Expand Down
Loading
Loading