From 2025a2a690f873981d2883e01fd6015e378c95d4 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:11:17 +0400 Subject: [PATCH] feat: better integrate with macos titlebar --- .vscode/settings.json | 24 +++++- components.json | 4 +- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 2 + src-tauri/capabilities/default.json | 5 ++ src-tauri/src/lib.rs | 120 +++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 10 +-- src/app/layout.tsx | 2 + src/app/page.tsx | 16 ++-- src/components/window-drag-area.tsx | 60 ++++++++++++++ tailwind.config.js | 13 +++ 11 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 src/components/window-drag-area.tsx create mode 100644 tailwind.config.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c0acf8..29dccb6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,45 @@ { "cSpell.words": [ + "applescript", + "autoconfig", "autologin", + "cdylib", "CFURL", + "checkin", "clippy", + "codegen", "donutbrowser", + "dtolnay", + "elif", + "gifs", "launchservices", "mountpoint", + "Mullvad", "nodecar", "ntlm", + "objc", + "osascript", + "plasmohq", "propertylist", + "reqwest", "rlib", "rustc", "serde", "shadcn", "signon", + "sonner", "sspi", "staticlib", + "swatinem", "sysinfo", "systempreferences", - "turbopack" + "tauri", + "titlebar", + "Torbrowser", + "turbopack", + "unlisten", + "wiremock", + "xattr", + "zhom" ] } diff --git a/components.json b/components.json index a450d2f..7bae856 100644 --- a/components.json +++ b/components.json @@ -4,7 +4,7 @@ "rsc": true, "tsx": true, "tailwind": { - "config": "", + "config": "tailwind.config.js", "css": "src/styles/globals.css", "baseColor": "zinc", "cssVariables": true, @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 472a497..85e8f03 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -971,6 +971,8 @@ dependencies = [ "directories", "futures-util", "lazy_static", + "objc2 0.6.1", + "objc2-app-kit", "reqwest", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d63aafb..7ed8d81 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,6 +37,8 @@ futures-util = "0.3" [target.'cfg(target_os = "macos")'.dependencies] core-foundation="0.10" +objc2 = "0.6.1" +objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] } [dev-dependencies] tempfile = "3.13.0" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2661420..c46d7b4 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,11 @@ "permissions": [ "core:default", "core:event:default", + "core:window:default", + "core:window:allow-start-dragging", + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", "opener:default", "fs:default", "shell:allow-execute", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e7d5e7c..3cb6c98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,7 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; -use tauri::{Emitter, Manager}; +use tauri::{Emitter, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; use tauri_plugin_deep_link::DeepLinkExt; // Store pending URLs that need to be handled when the window is ready @@ -58,6 +58,51 @@ use app_auto_updater::{ get_app_version_info, }; +// Trait to extend WebviewWindow with transparent titlebar functionality +pub trait WindowExt { + #[cfg(target_os = "macos")] + fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String>; +} + +impl WindowExt for WebviewWindow { + #[cfg(target_os = "macos")] + fn set_transparent_titlebar(&self, transparent: bool) -> Result<(), String> { + use objc2::rc::Retained; + use objc2_app_kit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility}; + + unsafe { + let ns_window: Retained = + Retained::retain(self.ns_window().unwrap().cast()).unwrap(); + + if transparent { + // Hide the title text + ns_window.setTitleVisibility(NSWindowTitleVisibility(2)); // NSWindowTitleHidden + + // Make titlebar transparent + ns_window.setTitlebarAppearsTransparent(true); + + // Set full size content view + let current_mask = ns_window.styleMask(); + let new_mask = NSWindowStyleMask(current_mask.0 | (1 << 15)); // NSFullSizeContentViewWindowMask + ns_window.setStyleMask(new_mask); + } else { + // Show the title text + ns_window.setTitleVisibility(NSWindowTitleVisibility(0)); // NSWindowTitleVisible + + // Make titlebar opaque + ns_window.setTitlebarAppearsTransparent(false); + + // Remove full size content view + let current_mask = ns_window.styleMask(); + let new_mask = NSWindowStyleMask(current_mask.0 & !(1 << 15)); + ns_window.setStyleMask(new_mask); + } + } + + Ok(()) + } +} + #[tauri::command] fn greet() -> String { let now = SystemTime::now(); @@ -124,6 +169,61 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result Result<(), String> { + #[cfg(target_os = "macos")] + { + if let Some(window) = app_handle.get_webview_window("main") { + use objc2::rc::Retained; + use objc2_app_kit::{NSColor, NSWindow}; + + let ns_window: Retained = + unsafe { Retained::retain(window.ns_window().unwrap().cast()).unwrap() }; + + let bg_color = if is_dark_mode { + // Dark mode - pure black background + unsafe { NSColor::colorWithRed_green_blue_alpha(0.0, 0.0, 0.0, 1.0) } + } else { + // Light mode - pure white background + unsafe { NSColor::colorWithRed_green_blue_alpha(1.0, 1.0, 1.0, 1.0) } + }; + + // Ensure this runs on the main thread for immediate visual update + unsafe { + // Set the window background color + ns_window.setBackgroundColor(Some(&bg_color)); + + // Force immediate visual updates using multiple refresh methods + ns_window.invalidateShadow(); + ns_window.display(); + + // Ensure the window content is redrawn + if let Some(content_view) = ns_window.contentView() { + content_view.setNeedsDisplay(true); + content_view.displayIfNeeded(); + } + + // Trigger a window update + ns_window.update(); + } + + // Also emit an event to the frontend to ensure synchronization + let _ = app_handle.emit("window-background-updated", is_dark_mode); + } + } + + #[cfg(not(target_os = "macos"))] + { + // For non-macOS platforms, we can't change the native window background + let _ = (app_handle, is_dark_mode); // Suppress unused variable warnings + } + + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -132,6 +232,23 @@ pub fn run() { .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_deep_link::init()) .setup(|app| { + // Create the main window programmatically + let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) + .title("Donut Browser") + .inner_size(900.0, 600.0) + .resizable(false) + .fullscreen(false); + + let window = win_builder.build().unwrap(); + + // Set transparent titlebar for macOS + #[cfg(target_os = "macos")] + { + if let Err(e) = window.set_transparent_titlebar(true) { + eprintln!("Failed to set transparent titlebar: {e}"); + } + } + // Set up deep link handler let handle = app.handle().clone(); @@ -264,6 +381,7 @@ pub fn run() { check_for_app_updates_manual, download_and_install_app_update, get_app_version_info, + set_window_background_color, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 48d365f..8dad49f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,15 +10,7 @@ "frontendDist": "../dist" }, "app": { - "windows": [ - { - "title": "Donut Browser", - "width": 900, - "height": 600, - "resizable": false, - "fullscreen": false - } - ], + "windows": [], "security": { "csp": null } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7a564a3..929b09d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "@/styles/globals.css"; import { CustomThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { WindowDragArea } from "@/components/window-drag-area"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -26,6 +27,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 35312a3..d147770 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -394,13 +394,13 @@ export default function Home() { ); return ( -
-
+
+
-
+
Profiles -
+
Settings @@ -423,9 +423,9 @@ export default function Home() { onClick={() => { setCreateProfileDialogOpen(true); }} - className="flex items-center gap-2" + className="flex gap-2 items-center" > - + Create a new profile diff --git a/src/components/window-drag-area.tsx b/src/components/window-drag-area.tsx new file mode 100644 index 0000000..fef1cbe --- /dev/null +++ b/src/components/window-drag-area.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { useEffect, useState } from "react"; + +export function WindowDragArea() { + const [isMacOS, setIsMacOS] = useState(false); + + useEffect(() => { + // Check if we're on macOS using user agent detection + const checkPlatform = () => { + const userAgent = navigator.userAgent.toLowerCase(); + setIsMacOS(userAgent.includes("mac")); + }; + + checkPlatform(); + }, []); + + const handleMouseDown = (e: React.MouseEvent) => { + // Only handle left mouse button + if (e.button !== 0) return; + + // Start dragging asynchronously + const startDrag = async () => { + try { + const window = getCurrentWindow(); + await window.startDragging(); + } catch (error) { + console.error("Failed to start window dragging:", error); + } + }; + + void startDrag(); + }; + + // Only render on macOS + if (!isMacOS) { + return null; + } + + return ( +
{ + e.preventDefault(); + }} + /> + ); +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..2c86fe2 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,13 @@ +module.exports = { + darkMode: "class", + theme: { + extend: { + colors: { + black: "#000000", + }, + backgroundColor: { + dark: "#000000", + }, + }, + }, +};