mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-09 14:25:12 +02:00
Add setting to use native window titlebar (#312)
This commit is contained in:
@@ -25,6 +25,7 @@ use tauri_plugin_deep_link::DeepLinkExt;
|
|||||||
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
|
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
|
||||||
use tauri_plugin_log::{Builder, Target, TargetKind, log};
|
use tauri_plugin_log::{Builder, Target, TargetKind, log};
|
||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||||
|
use yaak_mac_window::AppHandleMacWindowExt;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
@@ -1391,6 +1392,10 @@ pub fn run() {
|
|||||||
let grpc_handle = GrpcHandle::new(&app.app_handle());
|
let grpc_handle = GrpcHandle::new(&app.app_handle());
|
||||||
app.manage(Mutex::new(grpc_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());
|
monitor_plugin_events(&app.app_handle().clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
use yaak_models::query_manager::QueryManagerExt;
|
||||||
|
|
||||||
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
||||||
const DEFAULT_WINDOW_HEIGHT: f64 = 600.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")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
|
|||||||
@@ -4,16 +4,32 @@ mod commands;
|
|||||||
mod mac;
|
mod mac;
|
||||||
|
|
||||||
use crate::commands::{set_theme, set_title};
|
use crate::commands::{set_theme, set_title};
|
||||||
use tauri::{
|
use std::sync::atomic::AtomicBool;
|
||||||
Runtime, generate_handler,
|
use tauri::{generate_handler, plugin, plugin::TauriPlugin, Manager, Runtime};
|
||||||
plugin::{Builder, TauriPlugin},
|
|
||||||
};
|
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> {
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
#[allow(unused)]
|
plugin::Builder::new("yaak-mac-window")
|
||||||
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])
|
.invoke_handler(generate_handler![set_title, set_theme])
|
||||||
.on_window_ready(|window| {
|
.on_window_ready(move |window| {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
mac::setup_traffic_light_positioner(&window);
|
mac::setup_traffic_light_positioner(&window);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
|
use crate::PluginState;
|
||||||
use csscolorparser::Color;
|
use csscolorparser::Color;
|
||||||
use objc::{msg_send, sel, sel_impl};
|
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);
|
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) {
|
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
|
||||||
use cocoa::{appkit::NSWindow, base::nil, foundation::NSString};
|
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 {
|
unsafe {
|
||||||
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
|
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 win_title = NSString::alloc(nil).init_str(&title);
|
||||||
let handle = window_handle;
|
let handle = window_handle;
|
||||||
NSWindow::setTitle_(handle.0 as cocoa::base::id, win_title);
|
NSWindow::setTitle_(handle.0 as cocoa::base::id, win_title);
|
||||||
position_traffic_lights(
|
if !native_titlebar {
|
||||||
UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")),
|
position_traffic_lights(
|
||||||
WINDOW_CONTROL_PAD_X,
|
UnsafeWindowHandle(
|
||||||
WINDOW_CONTROL_PAD_Y,
|
window2.ns_window().expect("Failed to create window handle"),
|
||||||
label,
|
),
|
||||||
);
|
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 brightness = (color.r as f64 + color.g as f64 + color.b as f64) / 3.0;
|
||||||
let label = window.label().to_string();
|
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 {
|
unsafe {
|
||||||
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
|
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);
|
NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance);
|
||||||
position_traffic_lights(
|
if !native_titlebar {
|
||||||
UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")),
|
position_traffic_lights(
|
||||||
WINDOW_CONTROL_PAD_X,
|
UnsafeWindowHandle(
|
||||||
WINDOW_CONTROL_PAD_Y,
|
window2.ns_window().expect("Failed to create window handle"),
|
||||||
label,
|
),
|
||||||
);
|
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 rand::distr::Alphanumeric;
|
||||||
use std::ffi::c_void;
|
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(
|
position_traffic_lights(
|
||||||
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
|
UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")),
|
||||||
WINDOW_CONTROL_PAD_X,
|
WINDOW_CONTROL_PAD_X,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
|
|||||||
|
|
||||||
export type ProxySettingAuth = { user: string, password: string, };
|
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, };
|
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -112,6 +112,8 @@ pub struct Settings {
|
|||||||
pub editor_keymap: EditorKeymap,
|
pub editor_keymap: EditorKeymap,
|
||||||
pub editor_soft_wrap: bool,
|
pub editor_soft_wrap: bool,
|
||||||
pub hide_window_controls: 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: Option<String>,
|
||||||
pub interface_font_size: i32,
|
pub interface_font_size: i32,
|
||||||
pub interface_scale: f32,
|
pub interface_scale: f32,
|
||||||
@@ -168,6 +170,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
(InterfaceFontSize, self.interface_font_size.into()),
|
(InterfaceFontSize, self.interface_font_size.into()),
|
||||||
(InterfaceScale, self.interface_scale.into()),
|
(InterfaceScale, self.interface_scale.into()),
|
||||||
(HideWindowControls, self.hide_window_controls.into()),
|
(HideWindowControls, self.hide_window_controls.into()),
|
||||||
|
(UseNativeTitlebar, self.use_native_titlebar.into()),
|
||||||
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
|
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
|
||||||
(ThemeDark, self.theme_dark.as_str().into()),
|
(ThemeDark, self.theme_dark.as_str().into()),
|
||||||
(ThemeLight, self.theme_light.as_str().into()),
|
(ThemeLight, self.theme_light.as_str().into()),
|
||||||
@@ -193,6 +196,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
SettingsIden::InterfaceScale,
|
SettingsIden::InterfaceScale,
|
||||||
SettingsIden::InterfaceFont,
|
SettingsIden::InterfaceFont,
|
||||||
SettingsIden::HideWindowControls,
|
SettingsIden::HideWindowControls,
|
||||||
|
SettingsIden::UseNativeTitlebar,
|
||||||
SettingsIden::OpenWorkspaceNewWindow,
|
SettingsIden::OpenWorkspaceNewWindow,
|
||||||
SettingsIden::Proxy,
|
SettingsIden::Proxy,
|
||||||
SettingsIden::ThemeDark,
|
SettingsIden::ThemeDark,
|
||||||
@@ -225,6 +229,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
interface_font_size: row.get("interface_font_size")?,
|
interface_font_size: row.get("interface_font_size")?,
|
||||||
interface_scale: row.get("interface_scale")?,
|
interface_scale: row.get("interface_scale")?,
|
||||||
interface_font: row.get("interface_font")?,
|
interface_font: row.get("interface_font")?,
|
||||||
|
use_native_titlebar: row.get("use_native_titlebar")?,
|
||||||
open_workspace_new_window: row.get("open_workspace_new_window")?,
|
open_workspace_new_window: row.get("open_workspace_new_window")?,
|
||||||
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
|
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
|
||||||
theme_dark: row.get("theme_dark")?,
|
theme_dark: row.get("theme_dark")?,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ impl<'a> DbContext<'a> {
|
|||||||
interface_scale: 1.0,
|
interface_scale: 1.0,
|
||||||
interface_font: None,
|
interface_font: None,
|
||||||
hide_window_controls: false,
|
hide_window_controls: false,
|
||||||
|
use_native_titlebar: false,
|
||||||
open_workspace_new_window: None,
|
open_workspace_new_window: None,
|
||||||
proxy: None,
|
proxy: None,
|
||||||
theme_dark: "yaak-dark".to_string(),
|
theme_dark: "yaak-dark".to_string(),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function HeaderSize({
|
|||||||
}: HeaderSizeProps) {
|
}: HeaderSizeProps) {
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const isFullscreen = useIsFullscreen();
|
const isFullscreen = useIsFullscreen();
|
||||||
|
const nativeTitlebar = settings.useNativeTitlebar;
|
||||||
const finalStyle = useMemo<CSSProperties>(() => {
|
const finalStyle = useMemo<CSSProperties>(() => {
|
||||||
const s = { ...style };
|
const s = { ...style };
|
||||||
|
|
||||||
@@ -34,7 +35,9 @@ export function HeaderSize({
|
|||||||
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
|
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
|
||||||
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
|
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) {
|
if (!isFullscreen) {
|
||||||
// Add large padding for window controls
|
// Add large padding for window controls
|
||||||
s.paddingLeft = 72 / settings.interfaceScale;
|
s.paddingLeft = 72 / settings.interfaceScale;
|
||||||
@@ -51,6 +54,7 @@ export function HeaderSize({
|
|||||||
settings.interfaceScale,
|
settings.interfaceScale,
|
||||||
size,
|
size,
|
||||||
style,
|
style,
|
||||||
|
nativeTitlebar,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +77,7 @@ export function HeaderSize({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{!hideControls && <WindowControls onlyX={onlyXWindowControl} />}
|
{!hideControls && !nativeTitlebar && <WindowControls onlyX={onlyXWindowControl} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { useLicense } from '@yaakapp-internal/license';
|
|||||||
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
|
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
|
||||||
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||||
import { clamp } from '../../lib/clamp';
|
import { clamp } from '../../lib/clamp';
|
||||||
import { showConfirm } from '../../lib/confirm';
|
import { showConfirm } from '../../lib/confirm';
|
||||||
|
import { invokeCmd } from '../../lib/tauri';
|
||||||
import { CargoFeature } from '../CargoFeature';
|
import { CargoFeature } from '../CargoFeature';
|
||||||
|
import { Button } from '../core/Button';
|
||||||
import { Checkbox } from '../core/Checkbox';
|
import { Checkbox } from '../core/Checkbox';
|
||||||
import { Icon } from '../core/Icon';
|
import { Icon } from '../core/Icon';
|
||||||
import { Link } from '../core/Link';
|
import { Link } from '../core/Link';
|
||||||
@@ -154,6 +157,8 @@ export function SettingsInterface() {
|
|||||||
<LicenseSettings settings={settings} />
|
<LicenseSettings settings={settings} />
|
||||||
</CargoFeature>
|
</CargoFeature>
|
||||||
|
|
||||||
|
<NativeTitlebarSetting settings={settings} />
|
||||||
|
|
||||||
{type() !== 'macos' && (
|
{type() !== 'macos' && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.hideWindowControls}
|
checked={settings.hideWindowControls}
|
||||||
@@ -165,6 +170,33 @@ export function SettingsInterface() {
|
|||||||
</VStack>
|
</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 }) {
|
function LicenseSettings({ settings }: { settings: Settings }) {
|
||||||
const license = useLicense();
|
const license = useLicense();
|
||||||
if (license.check.data?.type !== 'personal_use') {
|
if (license.check.data?.type !== 'personal_use') {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function WindowControls({ className, onlyX }: Props) {
|
|||||||
const [maximized, setMaximized] = useState<boolean>(false);
|
const [maximized, setMaximized] = useState<boolean>(false);
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
// Never show controls on macOS or if hideWindowControls is true
|
// Never show controls on macOS or if hideWindowControls is true
|
||||||
if (type() === 'macos' || settings.hideWindowControls) {
|
if (type() === 'macos' || settings.hideWindowControls || settings.useNativeTitlebar) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user