Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions docs/shell-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ lights have a reserved 104px left area before the community switcher only in the
macOS desktop runtime. This inset does not move the centered tabs. 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. The main-window capability grants
only titlebar dragging and the internal native maximize action used by Tauri's drag
handler, plus scoped HTTP(S) opening for [external links](channels.md#run-the-integration).
See [Tauri window customization](https://v2.tauri.app/learn/window-customization/).
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
[external links](channels.md#run-the-integration). See
[Tauri window customization](https://v2.tauri.app/learn/window-customization/).

The top-right group contains enabled plugin launchers (Bestie supplies the snake),
a page finder, and the local avatar. `ProfileButton.tsx` subscribes to the community
Expand Down Expand Up @@ -103,7 +107,8 @@ behind, never over, opaque cards; it makes no relay request at runtime.
Run `just iterate` for UI changes and `just scan` for the broader review checks.
Check Home, Messages, and Settings; toggle a bundled plugin off/on and confirm its
navigation entry follows; inspect a narrow viewport. On macOS, verify titlebar
alignment, dragging, double-click zoom, and Settings access in a built app.
alignment, dragging, each macOS title-bar double-click preference, and Settings
access in a built app.

## Messages

Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ libc = "0.2"

[target.'cfg(target_os = "macos")'.dependencies]
mac-notification-sys = "=0.6.15"
objc2-foundation = { version = "0.3", default-features = false, features = ["NSString", "NSUserDefaults"] }

[target.'cfg(target_os = "windows")'.dependencies]
tauri-winrt-notification = "=0.7.3"
Expand Down
76 changes: 76 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,52 @@ use tauri_plugin_dialog::DialogExt;
#[derive(Clone, Default)]
struct Imports(Arc<Mutex<Option<PreparedImport>>>);

#[derive(Debug, PartialEq, Eq)]
enum TitleBarDoubleClickAction {
Maximize,
Minimize,
None,
}

fn title_bar_double_click_action(preference: Option<&str>) -> TitleBarDoubleClickAction {
match preference {
Some("Maximize" | "Zoom" | "Fill") => TitleBarDoubleClickAction::Maximize,
Some("Minimize") => TitleBarDoubleClickAction::Minimize,
_ => TitleBarDoubleClickAction::None,
}
}

#[tauri::command]
fn title_bar_double_click(window: tauri::Window) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
use objc2_foundation::{ns_string, NSUserDefaults};

let preference = NSUserDefaults::standardUserDefaults()
.stringForKey(ns_string!("AppleActionOnDoubleClick"))
.map(|value| value.to_string());
match title_bar_double_click_action(preference.as_deref()) {
TitleBarDoubleClickAction::Maximize => {
if window.is_maximized().map_err(|error| error.to_string())? {
window.unmaximize()
} else {
window.maximize()
}
.map_err(|error| error.to_string())?;
}
TitleBarDoubleClickAction::Minimize => {
window.minimize().map_err(|error| error.to_string())?;
}
TitleBarDoubleClickAction::None => {}
}
}

#[cfg(not(target_os = "macos"))]
let _ = window;

Ok(())
}

async fn prepare_import(
imports: Imports,
operation: impl FnOnce() -> Result<Option<PreparedImport>, String> + Send + 'static,
Expand Down Expand Up @@ -163,6 +209,7 @@ pub fn run() {
.manage(Notifications::default())
.manage(PluginManager(Manager::from_env()))
.invoke_handler(tauri::generate_handler![
title_bar_double_click,
notification_show,
terminal_create_owner,
terminal_spawn,
Expand Down Expand Up @@ -190,3 +237,32 @@ pub fn run() {
}
});
}

#[cfg(test)]
mod tests {
use super::{title_bar_double_click_action, TitleBarDoubleClickAction};

#[test]
fn title_bar_double_click_preferences_map_to_native_actions() {
assert_eq!(
title_bar_double_click_action(Some("Maximize")),
TitleBarDoubleClickAction::Maximize
);
assert_eq!(
title_bar_double_click_action(Some("Fill")),
TitleBarDoubleClickAction::Maximize
);
assert_eq!(
title_bar_double_click_action(Some("Minimize")),
TitleBarDoubleClickAction::Minimize
);
assert_eq!(
title_bar_double_click_action(Some("None")),
TitleBarDoubleClickAction::None
);
assert_eq!(
title_bar_double_click_action(None),
TitleBarDoubleClickAction::None
);
}
}
17 changes: 14 additions & 3 deletions src/app/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { ProfileButton } from "./ProfileButton";
import { PageSearch } from "./PageSearch";
import { orderPages, pagePresentation } from "./presentation";
import { PanelFrame } from "../../features/panels/PanelFrame";
import { macTitleBarDragHandlers } from "./title-bar";

const macDesktop = isTauri() && /Mac/i.test(navigator.platform);
const titleBarDragProps = macDesktop ? macTitleBarDragHandlers : {};

export function AppShell({
pages,
Expand Down Expand Up @@ -54,10 +56,15 @@ export function AppShell({
Skip to content
</a>
<header
data-tauri-drag-region
data-tauri-drag-region={macDesktop ? undefined : true}
{...titleBarDragProps}
className={`shell-header ${macDesktop ? "shell-header-mac" : ""}`}
>
<div className="shell-communities" data-tauri-drag-region>
<div
className="shell-communities"
data-tauri-drag-region={macDesktop ? undefined : true}
{...titleBarDragProps}
>
{navigationControls}
<CommunitySwitcher
communities={communities}
Expand Down Expand Up @@ -90,7 +97,11 @@ export function AppShell({
);
})}
</nav>
<div className="shell-actions" data-tauri-drag-region>
<div
className="shell-actions"
data-tauri-drag-region={macDesktop ? undefined : true}
{...titleBarDragProps}
>
{launchers}
<PageSearch pages={pages} onSelect={onSelect} />
<ProfileButton
Expand Down
76 changes: 76 additions & 0 deletions src/app/shell/title-bar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it, vi } from "vitest";
import { createTitleBarDragHandlers } from "./title-bar";

function mouseEvent(
detail: number,
overrides: Partial<{
button: number;
clientX: number;
clientY: number;
sameTarget: boolean;
}> = {},
) {
const currentTarget = {};
return {
button: overrides.button ?? 0,
clientX: overrides.clientX ?? 40,
clientY: overrides.clientY ?? 12,
currentTarget,
detail,
preventDefault: vi.fn(),
target: overrides.sameTarget === false ? {} : currentTarget,
} as unknown as Parameters<
ReturnType<typeof createTitleBarDragHandlers>["onMouseDown"]
>[0];
}

describe("macOS title bar dragging", () => {
it("starts dragging on a primary single click of the region itself", () => {
const actions = { startDragging: vi.fn(), doubleClick: vi.fn() };
const handlers = createTitleBarDragHandlers(actions);
const event = mouseEvent(1);

handlers.onMouseDown(event);

expect(event.preventDefault).toHaveBeenCalledOnce();
expect(actions.startDragging).toHaveBeenCalledOnce();
expect(actions.doubleClick).not.toHaveBeenCalled();
});

it("runs the native preference action after an unmoved double click", () => {
const actions = { startDragging: vi.fn(), doubleClick: vi.fn() };
const handlers = createTitleBarDragHandlers(actions);
const down = mouseEvent(2);
const up = mouseEvent(2);

handlers.onMouseDown(down);
handlers.onMouseUp(up);

expect(up.preventDefault).toHaveBeenCalledOnce();
expect(actions.doubleClick).toHaveBeenCalledOnce();
expect(actions.startDragging).not.toHaveBeenCalled();
});

it("cancels the double-click action after movement", () => {
const actions = { startDragging: vi.fn(), doubleClick: vi.fn() };
const handlers = createTitleBarDragHandlers(actions);

handlers.onMouseDown(mouseEvent(2));
handlers.onMouseUp(mouseEvent(2, { clientX: 41 }));

expect(actions.doubleClick).not.toHaveBeenCalled();
});

it("ignores interactive descendants and non-primary clicks", () => {
const actions = { startDragging: vi.fn(), doubleClick: vi.fn() };
const handlers = createTitleBarDragHandlers(actions);

handlers.onMouseDown(mouseEvent(1, { sameTarget: false }));
handlers.onMouseDown(mouseEvent(1, { button: 1 }));
handlers.onMouseDown(mouseEvent(2, { sameTarget: false }));
handlers.onMouseUp(mouseEvent(2, { sameTarget: false }));

expect(actions.startDragging).not.toHaveBeenCalled();
expect(actions.doubleClick).not.toHaveBeenCalled();
});
});
64 changes: 64 additions & 0 deletions src/app/shell/title-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { MouseEventHandler } from "react";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required Signed-off-by trailer

The reviewed commit has no Signed-off-by trailer, so it violates the repository's DCO requirement and will fail the hosted DCO Check. Ensure this commit carries the verified author's sign-off while preserving its actual authorship before integration.

AGENTS.md reference: AGENTS.md:L123-L126

Useful? React with 👍 / 👎.

import { invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";

type TitleBarActions = Readonly<{
startDragging: () => void;
doubleClick: () => void;
}>;

export type TitleBarDragHandlers = Readonly<{
onMouseDown: MouseEventHandler<HTMLElement>;
onMouseUp: MouseEventHandler<HTMLElement>;
}>;

export function createTitleBarDragHandlers(
actions: TitleBarActions,
): TitleBarDragHandlers {
let doubleClickStart: Readonly<{ x: number; y: number }> | undefined;

return {
onMouseDown(event) {
if (
event.button !== 0 ||
event.target !== event.currentTarget ||
(event.detail !== 1 && event.detail !== 2)
) {
return;
}

if (event.detail === 2) {
doubleClickStart = { x: event.clientX, y: event.clientY };
return;
}

event.preventDefault();
actions.startDragging();
},
onMouseUp(event) {
const start = doubleClickStart;
doubleClickStart = undefined;
if (
event.button !== 0 ||
event.detail !== 2 ||
event.target !== event.currentTarget ||
start?.x !== event.clientX ||
start.y !== event.clientY
Comment on lines +45 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow normal pointer slop on the second click

When the pointer moves by even one CSS pixel between the second mouse-down and mouse-up, these exact coordinate comparisons suppress the configured title-bar action even though event.detail === 2 shows that the platform accepted the interaction as a double-click. Normal hand jitter therefore makes Fill/Zoom or Minimize unreliable; use a small drag threshold or native double-click detection rather than requiring perfectly stationary coordinates.

Useful? React with 👍 / 👎.

) {
return;
}

event.preventDefault();
actions.doubleClick();
},
};
}

export const macTitleBarDragHandlers = createTitleBarDragHandlers({
startDragging: () => {
void getCurrentWindow().startDragging();
},
doubleClick: () => {
void invoke("title_bar_double_click");
},
});
Loading