mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(desktop): honor macOS title-bar double-click preference (#1674)
Signed-off-by: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -67,6 +67,91 @@ fn perform_sidebar_default_haptic() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs the window action matching the macOS "double-click a window's
|
||||
/// title bar to" preference (`AppleActionOnDoubleClick`).
|
||||
///
|
||||
/// macOS values are `Minimize`, `Maximize` (default when unset), `Fill`, or
|
||||
/// `None`.
|
||||
/// The desktop app uses a web-based title-bar drag region, so the frontend
|
||||
/// forwards double-clicks here and suppresses Tauri's injected drag-region
|
||||
/// handler, whose default macOS path hardcodes maximize.
|
||||
///
|
||||
/// For `Fill`, resize to the current monitor work area instead of using
|
||||
/// Tauri's maximize path, which maps to macOS zoom for titled, resizable
|
||||
/// windows.
|
||||
///
|
||||
/// On non-macOS platforms this always toggles maximize (the historical
|
||||
/// behavior).
|
||||
#[tauri::command]
|
||||
fn title_bar_double_click(window: tauri::Window) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let action = {
|
||||
let output = std::process::Command::new("defaults")
|
||||
.args(["read", "-g", "AppleActionOnDoubleClick"])
|
||||
.output();
|
||||
match output {
|
||||
Ok(output) if output.status.success() => {
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
}
|
||||
_ => "Maximize".to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
match action.as_str() {
|
||||
"None" => {}
|
||||
"Minimize" => {
|
||||
let _ = window.minimize();
|
||||
}
|
||||
"Fill" => {
|
||||
fill_window(&window);
|
||||
}
|
||||
// "Maximize" or any unexpected value.
|
||||
_ => {
|
||||
toggle_maximize(&window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
toggle_maximize(&window);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills the current display work area, excluding system UI like the menu bar
|
||||
/// and Dock.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn fill_window(window: &tauri::Window) {
|
||||
match window.current_monitor() {
|
||||
Ok(Some(monitor)) => {
|
||||
if window.is_maximized().unwrap_or(false) {
|
||||
let _ = window.unmaximize();
|
||||
}
|
||||
|
||||
let work_area = monitor.work_area();
|
||||
let _ = window.set_position(work_area.position);
|
||||
let _ = window.set_size(work_area.size);
|
||||
}
|
||||
_ => {
|
||||
let _ = window.maximize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the window between maximized and its previous size, matching the
|
||||
/// historical double-click behavior.
|
||||
fn toggle_maximize(window: &tauri::Window) {
|
||||
match window.is_maximized() {
|
||||
Ok(true) => {
|
||||
let _ = window.unmaximize();
|
||||
}
|
||||
_ => {
|
||||
let _ = window.maximize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default()
|
||||
@@ -441,6 +526,7 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
title_bar_double_click,
|
||||
get_identity,
|
||||
get_nsec,
|
||||
import_identity,
|
||||
|
||||
@@ -11,10 +11,65 @@ export type AppView =
|
||||
| "projects";
|
||||
|
||||
const WINDOW_DRAG_HANDLE_HEIGHT = 44;
|
||||
const TAURI_DRAG_REGION_ATTR = "data-tauri-drag-region";
|
||||
const WINDOW_DRAG_INTERACTIVE_SELECTOR =
|
||||
'button, a, input, textarea, select, [role="button"], [contenteditable="true"]';
|
||||
'button, a, input, textarea, select, label, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="option"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
const CLICKABLE_TAGS = new Set([
|
||||
"A",
|
||||
"BUTTON",
|
||||
"INPUT",
|
||||
"SELECT",
|
||||
"TEXTAREA",
|
||||
"LABEL",
|
||||
"SUMMARY",
|
||||
]);
|
||||
const INTERACTIVE_ROLES = new Set([
|
||||
"button",
|
||||
"link",
|
||||
"menuitem",
|
||||
"tab",
|
||||
"checkbox",
|
||||
"radio",
|
||||
"switch",
|
||||
"option",
|
||||
]);
|
||||
|
||||
function isClickableElement(element: HTMLElement) {
|
||||
return (
|
||||
CLICKABLE_TAGS.has(element.tagName) ||
|
||||
(element.hasAttribute("contenteditable") &&
|
||||
element.getAttribute("contenteditable") !== "false") ||
|
||||
(element.hasAttribute("tabindex") &&
|
||||
element.getAttribute("tabindex") !== "-1") ||
|
||||
INTERACTIVE_ROLES.has(element.getAttribute("role") ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function isTauriDragRegionEvent(event: MouseEvent | PointerEvent) {
|
||||
const path = event.composedPath();
|
||||
const directTarget = path[0];
|
||||
|
||||
for (const item of path) {
|
||||
if (!(item instanceof HTMLElement)) continue;
|
||||
|
||||
const attr = item.getAttribute(TAURI_DRAG_REGION_ATTR);
|
||||
|
||||
if (isClickableElement(item) && attr === null) return false;
|
||||
if (attr === null) continue;
|
||||
if (attr === "false") return false;
|
||||
if (attr === "deep") return true;
|
||||
if (attr === "" || attr === "true") return item === directTarget;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isWindowDragHandleEvent(event: MouseEvent | PointerEvent) {
|
||||
if (isTauriDragRegionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.clientY > WINDOW_DRAG_HANDLE_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import { isWindowDragHandleEvent } from "@/app/AppShell.helpers";
|
||||
import { performTitleBarDoubleClickAction } from "@/shared/lib/titleBarActions";
|
||||
|
||||
export function useTauriWindowDrag() {
|
||||
React.useEffect(() => {
|
||||
@@ -17,19 +18,35 @@ export function useTauriWindowDrag() {
|
||||
void getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
function stopTauriDragRegionHandler(event: MouseEvent) {
|
||||
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tauri's injected data-tauri-drag-region listener hardcodes maximize on
|
||||
// double-click. Buzz handles drag and double-click itself so macOS title
|
||||
// bar preferences can be respected.
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
function handleDoubleClick(event: MouseEvent) {
|
||||
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void getCurrentWindow().toggleMaximize();
|
||||
event.stopImmediatePropagation();
|
||||
void performTitleBarDoubleClickAction();
|
||||
}
|
||||
|
||||
window.addEventListener("pointerdown", handlePointerDown, true);
|
||||
window.addEventListener("mousedown", stopTauriDragRegionHandler, true);
|
||||
window.addEventListener("mouseup", stopTauriDragRegionHandler, true);
|
||||
window.addEventListener("dblclick", handleDoubleClick, true);
|
||||
return () => {
|
||||
window.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
window.removeEventListener("mousedown", stopTauriDragRegionHandler, true);
|
||||
window.removeEventListener("mouseup", stopTauriDragRegionHandler, true);
|
||||
window.removeEventListener("dblclick", handleDoubleClick, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
22
desktop/src/shared/lib/titleBarActions.ts
Normal file
22
desktop/src/shared/lib/titleBarActions.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
/**
|
||||
* Runs the window action that matches the OS "double-click a window's title
|
||||
* bar to" preference (macOS `AppleActionOnDoubleClick`).
|
||||
*
|
||||
* The app uses a web-based title-bar drag region, so the OS does not act on a
|
||||
* title-bar double-click on its own. The Rust `title_bar_double_click` command
|
||||
* reads the preference and dispatches a single matching action (native
|
||||
* `miniaturize:` for minimize, so the window genies into the Dock).
|
||||
*/
|
||||
export async function performTitleBarDoubleClickAction(): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("title_bar_double_click");
|
||||
} catch {
|
||||
// No-op on failure; a missed titlebar action is not worth surfacing.
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import * as React from "react";
|
||||
|
||||
import { performTitleBarDoubleClickAction } from "@/shared/lib/titleBarActions";
|
||||
|
||||
const WINDOW_DRAG_HANDLE_HEIGHT = 44;
|
||||
const WINDOW_DRAG_INTERACTIVE_SELECTOR =
|
||||
'button, a, input, textarea, select, [role="button"], [contenteditable="true"]';
|
||||
'button, a, input, textarea, select, label, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="option"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
function isWindowDragHandleEvent(event: MouseEvent | PointerEvent) {
|
||||
if (event.clientY > WINDOW_DRAG_HANDLE_HEIGHT) {
|
||||
@@ -31,19 +33,32 @@ export function StartupWindowDragRegion() {
|
||||
void getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
function stopTauriDragRegionHandler(event: MouseEvent) {
|
||||
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
function handleDoubleClick(event: MouseEvent) {
|
||||
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void getCurrentWindow().toggleMaximize();
|
||||
event.stopImmediatePropagation();
|
||||
void performTitleBarDoubleClickAction();
|
||||
}
|
||||
|
||||
window.addEventListener("pointerdown", handlePointerDown, true);
|
||||
window.addEventListener("mousedown", stopTauriDragRegionHandler, true);
|
||||
window.addEventListener("mouseup", stopTauriDragRegionHandler, true);
|
||||
window.addEventListener("dblclick", handleDoubleClick, true);
|
||||
return () => {
|
||||
window.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
window.removeEventListener("mousedown", stopTauriDragRegionHandler, true);
|
||||
window.removeEventListener("mouseup", stopTauriDragRegionHandler, true);
|
||||
window.removeEventListener("dblclick", handleDoubleClick, true);
|
||||
};
|
||||
}, []);
|
||||
@@ -52,7 +67,6 @@ export function StartupWindowDragRegion() {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed inset-x-0 top-0 z-20 h-10 select-none"
|
||||
data-tauri-drag-region
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user