Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 10 additions & 6 deletions docs/shell-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,19 @@ motion with Tailwind's `motion-reduce` variant.

Tauri uses `titleBarStyle: Overlay` and `hiddenTitle` on macOS. Native traffic
lights have a reserved 104px left area before the community switcher only in the
macOS desktop runtime. Web gets no
inset or imitation window controls. Other
platforms retain their native decorations. Drag regions are limited to the
header background; controls remain clickable. On macOS, double-clicking that
macOS desktop runtime. Web gets no inset or imitation window controls. Linux
and Windows desktop use undecorated windows with app-owned minimize,
maximize/restore and close buttons in the header, including during identity
setup. Drag regions are limited to the header background; controls remain
clickable. On macOS, double-clicking that
background follows the current system title-bar preference (Fill/Zoom, Minimize,
or no action); changing the preference does not require restarting Buzz. Other
platforms retain Tauri's native drag-region behavior. The main-window capability
grants only titlebar dragging and the internal maximize action used by that
handler, plus scoped HTTP(S) opening for
grants titlebar dragging and the internal maximize action used by that
handler. A Linux and Windows main-webview capability also grants minimize,
maximize/restore and close for the integrated controls. These actions do not
change the app’s existing close lifecycle. The main capability includes scoped
HTTP(S) opening for
[external links](channels.md#run-the-integration). See
[Tauri window customization](https://v2.tauri.app/learn/window-customization/).

Expand Down
12 changes: 12 additions & 0 deletions src-tauri/capabilities/desktop-window.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-window",
"description": "Linux and Windows integrated window controls, restricted to the main local webview.",
"platforms": ["linux", "windows"],
"webviews": ["main"],
"permissions": [
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close"
]
}
15 changes: 15 additions & 0 deletions src-tauri/tauri.linux.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"app": {
"windows": [
{
"title": "Buzz Foundation",
"dragDropEnabled": false,
"width": 1200,
"height": 800,
"decorations": false,
"minWidth": 480,
"minHeight": 400
}
]
}
}
15 changes: 15 additions & 0 deletions src-tauri/tauri.windows.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"app": {
"windows": [
{
"title": "Buzz Foundation",
"dragDropEnabled": false,
"width": 1200,
"height": 800,
"decorations": false,
"minWidth": 480,
"minHeight": 400
}
]
}
}
2 changes: 2 additions & 0 deletions src/app/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PageSearch, type SearchServices } from "./PageSearch";
import { orderPages, pagePresentation } from "./presentation";
import { PanelFrame } from "../../features/panels/PanelFrame";
import { macTitleBarDragHandlers } from "./title-bar";
import { WindowControls } from "./WindowControls";

const macDesktop = isTauri() && /Mac/i.test(navigator.platform);
const titleBarDragProps = macDesktop ? macTitleBarDragHandlers : {};
Expand Down Expand Up @@ -200,6 +201,7 @@ export function AppShell({
onSettings={() => onSelect("settings")}
onProfile={onProfile}
/>
<WindowControls />
</div>
</header>

Expand Down
69 changes: 69 additions & 0 deletions src/app/shell/WindowControls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, expect, it, vi } from "vitest";
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { WindowControls } from "./WindowControls";

vi.mock("@tauri-apps/api/core", () => ({ isTauri: vi.fn() }));
vi.mock("@tauri-apps/api/window", () => ({ getCurrentWindow: vi.fn() }));
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});

it.each([
[false, "Linux x86_64"],
[false, "Win32"],
[true, "MacIntel"],
[true, "Darwin"],
])(
"does not render outside native Linux or Windows (%s, %s)",
(native, platform) => {
vi.mocked(isTauri).mockReturnValue(native);
vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
render(<WindowControls />);
expect(screen.queryByRole("group")).not.toBeInTheDocument();
expect(getCurrentWindow).not.toHaveBeenCalled();
},
);

it.each(["Linux x86_64", "Win32"])(
"supports keyboard window actions and retry on %s",
async (platform) => {
vi.mocked(isTauri).mockReturnValue(true);
vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
const minimize = vi
.fn()
.mockRejectedValueOnce(new Error("denied"))
.mockResolvedValue(undefined);
const toggleMaximize = vi.fn().mockResolvedValue(undefined);
const close = vi.fn().mockResolvedValue(undefined);
vi.mocked(getCurrentWindow).mockReturnValue({
minimize,
toggleMaximize,
close,
} as unknown as ReturnType<typeof getCurrentWindow>);
const user = userEvent.setup();
render(<WindowControls />);
await user.tab();
expect(
screen.getByRole("button", { name: "Minimize window" }),
).toHaveFocus();
await user.keyboard("{Enter}");
expect(await screen.findByRole("alert")).toHaveTextContent(
"Window action failed",
);
await user.keyboard("{Enter}");
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
await user.tab();
await user.keyboard("{Enter}");
await user.tab();
await user.keyboard(" ");
expect(minimize).toHaveBeenCalledTimes(2);
expect(toggleMaximize).toHaveBeenCalledOnce();
expect(close).toHaveBeenCalledOnce();
},
);
60 changes: 60 additions & 0 deletions src/app/shell/WindowControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import {
ArrowsOutIcon,
MinusIcon,
XIcon,
} from "../../shared/design-system/icons";
import { IconButton } from "../../shared/design-system/ui/IconButton";

export function hasIntegratedWindowControls() {
return isTauri() && /^(Linux|Win)/i.test(navigator.platform);
}

/** App-owned chrome shared by the shell and pre-identity screen. */
export function WindowControls() {
const [error, setError] = useState<string>();
if (!hasIntegratedWindowControls()) return null;
const run = async (action: () => Promise<void>) => {
setError(undefined);
try {
await action();
} catch {
setError("Window action failed. Try again.");
}
};
return (
<fieldset
className="flex shrink-0 items-center gap-1"
aria-label="Window controls"
>
{error && (
<span role="alert" className="text-body-sm">
{error}
</span>
)}
<IconButton
aria-label="Minimize window"
title="Minimize window"
size="toolbar"
icon={<MinusIcon size={16} />}
onClick={() => void run(() => getCurrentWindow().minimize())}
/>
<IconButton
aria-label="Maximize or restore window"
title="Maximize or restore window"
size="toolbar"
icon={<ArrowsOutIcon size={16} />}
onClick={() => void run(() => getCurrentWindow().toggleMaximize())}
/>
<IconButton
aria-label="Close window"
title="Close window"
size="toolbar"
icon={<XIcon size={16} />}
onClick={() => void run(() => getCurrentWindow().close())}
/>
</fieldset>
);
}
17 changes: 13 additions & 4 deletions src/features/identity/IdentitySetup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useState, useSyncExternalStore, type ReactNode } from "react";
import { isTauri } from "@tauri-apps/api/core";
import {
hasIntegratedWindowControls,
WindowControls,
} from "../../app/shell/WindowControls";
import { macTitleBarDragHandlers } from "../../app/shell/title-bar";
import { Button } from "../../shared/design-system/ui/Button";
import { Input } from "../../shared/design-system/ui/Input";
Expand All @@ -15,14 +19,19 @@ export function IdentitySetup({
const state = useSyncExternalStore(identity.subscribe, identity.snapshot);
const [importing, setImporting] = useState(false);
const [nsec, setNsec] = useState("");
const macDesktop = isTauri() && /Mac/i.test(navigator.platform);
const integratedWindowControls = hasIntegratedWindowControls();
if (state.status === "ready") return children;
return (
<main className="p-8" aria-labelledby="identity-setup-title">
{isTauri() && /Mac/i.test(navigator.platform) && (
{(macDesktop || integratedWindowControls) && (
<header
className="-mx-8 -mt-8 mb-8 h-14"
{...macTitleBarDragHandlers}
/>
className="-mx-8 -mt-8 mb-8 flex h-14 items-center justify-end px-3"
data-tauri-drag-region={integratedWindowControls || undefined}
{...(macDesktop ? macTitleBarDragHandlers : {})}
>
<WindowControls />
</header>
)}
<h1 id="identity-setup-title" className="text-heading">
Your Buzz identity
Expand Down
20 changes: 17 additions & 3 deletions src/features/identity/identity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ it("keeps native titlebar actions available throughout identity setup without dr
});

it.each([
[true, "Win32"],
[true, "Linux x86_64"],
[false, "Win32"],
[false, "Linux x86_64"],
[false, "MacIntel"],
])("does not add a macOS titlebar for native=%s on %s", (native, platform) => {
])("does not add a titlebar for native=%s on %s", (native, platform) => {
vi.mocked(isTauri).mockReturnValue(native);
vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
vi.mocked(invoke).mockReturnValue(new Promise(() => {}));
Expand Down Expand Up @@ -286,3 +286,17 @@ it("enables packaged identity on desktop platforms but never in the live broker
vi.spyOn(navigator, "platform", "get").mockReturnValue("iPhone");
expect(nativeIdentityEnabled()).toBe(false);
});

it.each(["Linux x86_64", "Win32"])(
"keeps %s window controls available before identity restoration completes",
(platform) => {
vi.mocked(isTauri).mockReturnValue(true);
vi.spyOn(navigator, "platform", "get").mockReturnValue(platform);
vi.mocked(invoke).mockReturnValue(new Promise(() => {}));
const identity = createIdentity();
render(<IdentitySetup identity={identity}>Signed in</IdentitySetup>);
expect(titleBar()).toHaveAttribute("data-tauri-drag-region", "true");
expect(screen.getByRole("button", { name: "Close window" })).toBeEnabled();
identity.dispose();
},
);
Loading