Add setting to use native window titlebar (#312)

This commit is contained in:
Gregory Schier
2025-12-05 09:15:48 -08:00
committed by GitHub
parent 095af8cf4b
commit 5bf7278479
11 changed files with 111 additions and 25 deletions

View File

@@ -25,6 +25,7 @@ use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind, log};
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use yaak_mac_window::AppHandleMacWindowExt;
use tokio::sync::Mutex;
use tokio::task::block_in_place;
use tokio::time;
@@ -1391,6 +1392,10 @@ pub fn run() {
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
// Specific settings
let settings = app.db().get_settings();
app.app_handle().set_native_titlebar(settings.use_native_titlebar);
monitor_plugin_events(&app.app_handle().clone());
Ok(())

View File

@@ -7,6 +7,7 @@ use tauri::{
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
use yaak_models::query_manager::QueryManagerExt;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
@@ -94,7 +95,8 @@ pub(crate) fn create_window<R: Runtime>(
});
}
if config.hide_titlebar {
let settings = handle.db().get_settings();
if config.hide_titlebar && !settings.use_native_titlebar {
#[cfg(target_os = "macos")]
{
use tauri::TitleBarStyle;

View File

@@ -4,16 +4,32 @@ mod commands;
mod mac;
use crate::commands::{set_theme, set_title};
use tauri::{
Runtime, generate_handler,
plugin::{Builder, TauriPlugin},
};
use std::sync::atomic::AtomicBool;
use tauri::{generate_handler, plugin, plugin::TauriPlugin, Manager, Runtime};
pub trait AppHandleMacWindowExt {
/// Sets whether to use the native titlebar
fn set_native_titlebar(&self, enable: bool);
}
impl<R: Runtime> AppHandleMacWindowExt for tauri::AppHandle<R> {
fn set_native_titlebar(&self, enable: bool) {
self.state::<PluginState>().native_titlebar.store(enable, std::sync::atomic::Ordering::Relaxed);
}
}
pub(crate) struct PluginState {
native_titlebar: AtomicBool,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
#[allow(unused)]
Builder::new("yaak-mac-window")
plugin::Builder::new("yaak-mac-window")
.setup(move |app, _| {
app.manage(PluginState { native_titlebar: AtomicBool::new(false) });
Ok(())
})
.invoke_handler(generate_handler![set_title, set_theme])
.on_window_ready(|window| {
.on_window_ready(move |window| {
#[cfg(target_os = "macos")]
{
mac::setup_traffic_light_positioner(&window);

View File

@@ -1,7 +1,8 @@
#![allow(deprecated)]
use crate::PluginState;
use csscolorparser::Color;
use objc::{msg_send, sel, sel_impl};
use tauri::{Emitter, Runtime, Window};
use tauri::{Emitter, Manager, Runtime, State, Window};
struct UnsafeWindowHandle(*mut std::ffi::c_void);
@@ -16,6 +17,8 @@ const MAIN_WINDOW_PREFIX: &str = "main_";
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
use cocoa::{appkit::NSWindow, base::nil, foundation::NSString};
let state: State<PluginState> = window.state();
let native_titlebar = state.native_titlebar.load(std::sync::atomic::Ordering::Relaxed);
unsafe {
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
@@ -25,12 +28,16 @@ pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String)
let win_title = NSString::alloc(nil).init_str(&title);
let handle = window_handle;
NSWindow::setTitle_(handle.0 as cocoa::base::id, win_title);
position_traffic_lights(
UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
if !native_titlebar {
position_traffic_lights(
UnsafeWindowHandle(
window2.ns_window().expect("Failed to create window handle"),
),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
}
});
}
}
@@ -42,6 +49,8 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: Color) {
let brightness = (color.r as f64 + color.g as f64 + color.b as f64) / 3.0;
let label = window.label().to_string();
let state: State<PluginState> = window.state();
let native_titlebar = state.native_titlebar.load(std::sync::atomic::Ordering::Relaxed);
unsafe {
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
@@ -56,12 +65,16 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: Color) {
};
NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance);
position_traffic_lights(
UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
if !native_titlebar {
position_traffic_lights(
UnsafeWindowHandle(
window2.ns_window().expect("Failed to create window handle"),
),
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
label,
);
}
});
}
}
@@ -119,6 +132,11 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: &Window<R>) {
use rand::distr::Alphanumeric;
use std::ffi::c_void;
let state: State<PluginState> = window.state();
if state.native_titlebar.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
position_traffic_lights(
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
WINDOW_CONTROL_PAD_X,

View File

@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -0,0 +1,3 @@
-- Add a setting to force native window title bar / controls
ALTER TABLE settings
ADD COLUMN use_native_titlebar BOOLEAN DEFAULT FALSE NOT NULL;

View File

@@ -112,6 +112,8 @@ pub struct Settings {
pub editor_keymap: EditorKeymap,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
// When true (primarily on Windows/Linux), use the native OS window title bar and controls
pub use_native_titlebar: bool,
pub interface_font: Option<String>,
pub interface_font_size: i32,
pub interface_scale: f32,
@@ -168,6 +170,7 @@ impl UpsertModelInfo for Settings {
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
(UseNativeTitlebar, self.use_native_titlebar.into()),
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
@@ -193,6 +196,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::InterfaceScale,
SettingsIden::InterfaceFont,
SettingsIden::HideWindowControls,
SettingsIden::UseNativeTitlebar,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::ThemeDark,
@@ -225,6 +229,7 @@ impl UpsertModelInfo for Settings {
interface_font_size: row.get("interface_font_size")?,
interface_scale: row.get("interface_scale")?,
interface_font: row.get("interface_font")?,
use_native_titlebar: row.get("use_native_titlebar")?,
open_workspace_new_window: row.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,

View File

@@ -26,6 +26,7 @@ impl<'a> DbContext<'a> {
interface_scale: 1.0,
interface_font: None,
hide_window_controls: false,
use_native_titlebar: false,
open_workspace_new_window: None,
proxy: None,
theme_dark: "yaak-dark".to_string(),

View File

@@ -27,6 +27,7 @@ export function HeaderSize({
}: HeaderSizeProps) {
const settings = useAtomValue(settingsAtom);
const isFullscreen = useIsFullscreen();
const nativeTitlebar = settings.useNativeTitlebar;
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
@@ -34,7 +35,9 @@ export function HeaderSize({
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (type() === 'macos') {
if (nativeTitlebar) {
// No style updates when using native titlebar
} else if (type() === 'macos') {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 72 / settings.interfaceScale;
@@ -51,6 +54,7 @@ export function HeaderSize({
settings.interfaceScale,
size,
style,
nativeTitlebar,
]);
return (
@@ -73,7 +77,7 @@ export function HeaderSize({
>
{children}
</div>
{!hideControls && <WindowControls onlyX={onlyXWindowControl} />}
{!hideControls && !nativeTitlebar && <WindowControls onlyX={onlyXWindowControl} />}
</div>
);
}

View File

@@ -4,11 +4,14 @@ import { useLicense } from '@yaakapp-internal/license';
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { showConfirm } from '../../lib/confirm';
import { invokeCmd } from '../../lib/tauri';
import { CargoFeature } from '../CargoFeature';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
@@ -154,6 +157,8 @@ export function SettingsInterface() {
<LicenseSettings settings={settings} />
</CargoFeature>
<NativeTitlebarSetting settings={settings} />
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
@@ -165,6 +170,33 @@ export function SettingsInterface() {
</VStack>
);
}
function NativeTitlebarSetting({ settings }: { settings: Settings }) {
const [nativeTitlebar, setNativeTitlebar] = useState(settings.useNativeTitlebar);
return (
<div className="flex gap-1 overflow-hidden h-2xs">
<Checkbox
checked={nativeTitlebar}
title="Native title bar"
help="Use the operating system's standard title bar and window controls"
onChange={setNativeTitlebar}
/>
{settings.useNativeTitlebar !== nativeTitlebar && (
<Button
color="primary"
size="2xs"
onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd('cmd_restart');
}}
>
Apply and Restart
</Button>
)}
</div>
);
}
function LicenseSettings({ settings }: { settings: Settings }) {
const license = useLicense();
if (license.check.data?.type !== 'personal_use') {

View File

@@ -18,7 +18,7 @@ export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const settings = useAtomValue(settingsAtom);
// Never show controls on macOS or if hideWindowControls is true
if (type() === 'macos' || settings.hideWindowControls) {
if (type() === 'macos' || settings.hideWindowControls || settings.useNativeTitlebar) {
return null;
}