Allow disabling window decorations/controls (#176)

Co-authored-by: Gregory Schier <gschier1990@gmail.com>
This commit is contained in:
Desperate Necromancer
2025-05-17 03:33:59 +07:00
committed by GitHub
parent 432b366105
commit be82b67ed3
17 changed files with 75 additions and 58 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN hide_window_controls BOOLEAN DEFAULT FALSE NOT NULL;

View File

@@ -58,7 +58,7 @@ export type ProxySetting = { "type": "enabled", disabled: boolean, http: string,
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
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

@@ -1,9 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models";
import type { Folder } from "./gen_models";
import type { GrpcRequest } from "./gen_models";
import type { HttpRequest } from "./gen_models";
import type { WebsocketRequest } from "./gen_models";
import type { Workspace } from "./gen_models";
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

View File

@@ -105,6 +105,7 @@ pub struct Settings {
pub appearance: String,
pub editor_font_size: i32,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
pub interface_font_size: i32,
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
@@ -154,6 +155,7 @@ impl UpsertModelInfo for Settings {
(EditorSoftWrap, self.editor_soft_wrap.into()),
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
@@ -171,6 +173,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::EditorSoftWrap,
SettingsIden::InterfaceFontSize,
SettingsIden::InterfaceScale,
SettingsIden::HideWindowControls,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::ThemeDark,
@@ -200,6 +203,7 @@ impl UpsertModelInfo for Settings {
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,
theme_light: row.get("theme_light")?,
hide_window_controls: row.get("hide_window_controls")?,
update_channel: row.get("update_channel")?,
})
}

View File

@@ -23,6 +23,7 @@ impl<'a> DbContext<'a> {
editor_soft_wrap: true,
interface_font_size: 15,
interface_scale: 1.0,
hide_window_controls: false,
open_workspace_new_window: None,
proxy: None,
theme_dark: "yaak-dark".to_string(),

View File

@@ -1,9 +1,8 @@
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { HTMLAttributes, ReactNode } from 'react';
import React from 'react';
import { useOsInfo } from '../hooks/useOsInfo';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import React, { useMemo } from 'react';
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
@@ -23,27 +22,42 @@ export function HeaderSize({
onlyXWindowControl,
children,
}: HeaderSizeProps) {
const osInfo = useOsInfo();
const settings = useAtomValue(settingsAtom);
const stoplightsVisible = useStoplightsVisible();
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
// Set the height (use min-height because scaling font size may make it larger
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
// Add large padding for window controls
if (stoplightsVisible && !ignoreControlsSpacing) {
s.paddingLeft = 72 / settings.interfaceScale;
} else if (!stoplightsVisible && !ignoreControlsSpacing && !settings.hideWindowControls) {
s.paddingRight = WINDOW_CONTROLS_WIDTH;
}
return s;
}, [
ignoreControlsSpacing,
settings.hideWindowControls,
settings.interfaceScale,
size,
stoplightsVisible,
style,
]);
return (
<div
data-tauri-drag-region
style={{
...style,
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
paddingLeft:
stoplightsVisible && !ignoreControlsSpacing ? 72 / settings.interfaceScale : undefined,
...(size === 'md' ? { minHeight: HEADER_SIZE_MD } : {}),
...(size === 'lg' ? { minHeight: HEADER_SIZE_LG } : {}),
...(osInfo.osType === 'macos' || ignoreControlsSpacing
? { paddingRight: '2px' }
: { paddingLeft: '2px', paddingRight: WINDOW_CONTROLS_WIDTH }),
}}
style={finalStyle}
className={classNames(
className,
'px-1', // Give it some space on either end
'pt-[1px]', // Make up for bottom border
'select-none relative',
'pt-[1px] w-full border-b border-border-subtle min-w-0',
'w-full border-b border-border-subtle min-w-0',
)}
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}

View File

@@ -1,9 +1,9 @@
import { useSearch } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useOsInfo } from '../../hooks/useOsInfo';
import { capitalize } from '../../lib/capitalize';
import { HStack } from '../core/Stacks';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
@@ -27,7 +27,6 @@ const tabs = [TAB_GENERAL, TAB_APPEARANCE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE]
export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
const osInfo = useOsInfo();
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
const [tab, setTab] = useState<string | undefined>(tabFromQuery);
@@ -60,9 +59,7 @@ export default function Settings({ hide }: Props) {
justifyContent="center"
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
>
<div className={classNames(osInfo?.osType === 'macos' ? 'text-center' : 'pl-2')}>
Settings
</div>
<div className={classNames(type() === 'macos' ? 'text-center' : 'pl-2')}>Settings</div>
</HStack>
</HeaderSize>
)}

View File

@@ -18,6 +18,7 @@ import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
import { type } from '@tauri-apps/plugin-os';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
@@ -122,6 +123,15 @@ export function SettingsAppearance() {
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide Window Controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
<Separator className="my-4" />
<Select

View File

@@ -1,8 +1,10 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import React, { useState } from 'react';
import { useOsInfo } from '../hooks/useOsInfo';
import {WINDOW_CONTROLS_WIDTH} from "../lib/constants";
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { Button } from './core/Button';
import { HStack } from './core/Stacks';
@@ -14,10 +16,9 @@ interface Props {
export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const osInfo = useOsInfo();
// Never show controls on macOS
if (osInfo.osType === 'macos') {
const settings = useAtomValue(settingsAtom);
// Never show controls on macOS or if hideWindowControls is true
if (type() === 'macos' || settings.hideWindowControls) {
return null;
}

View File

@@ -31,10 +31,10 @@ export function Checkbox({
<HStack
as="label"
alignItems="center"
space={3}
space={2}
className={classNames(className, 'text-text mr-auto')}
>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex mr-0.5')}>
<input
aria-hidden
className={classNames(

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { useFormattedHotkey } from '../../hooks/useHotKey';
import { useOsInfo } from '../../hooks/useOsInfo';
import { HStack } from './Stacks';
interface Props {
@@ -11,9 +10,8 @@ interface Props {
}
export function HotKey({ action, className, variant }: Props) {
const osInfo = useOsInfo();
const labelParts = useFormattedHotkey(action);
if (labelParts === null || osInfo == null) {
if (labelParts === null) {
return null;
}

View File

@@ -1,13 +1,13 @@
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import { useState } from 'react';
import { useOsInfo } from '../../hooks/useOsInfo';
import type { ButtonProps } from './Button';
import { Button } from './Button';
import { Label } from './Label';
import type { RadioDropdownItem } from './RadioDropdown';
import { RadioDropdown } from './RadioDropdown';
import { HStack } from './Stacks';
import { type } from '@tauri-apps/plugin-os';
export interface SelectProps<T extends string> {
name: string;
@@ -40,7 +40,6 @@ export function Select<T extends string>({
defaultValue,
size = 'md',
}: SelectProps<T>) {
const osInfo = useOsInfo();
const [focused, setFocused] = useState<boolean>(false);
const id = `input-${name}`;
const isInvalidSelection = options.find((o) => 'value' in o && o.value === value) == null;
@@ -63,7 +62,7 @@ export function Select<T extends string>({
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName}>
{label}
</Label>
{osInfo?.osType === 'macos' ? (
{type() === 'macos' ? (
<HStack
space={2}
className={classNames(

View File

@@ -2,7 +2,6 @@ import { type } from '@tauri-apps/plugin-os';
import { debounce } from '@yaakapp-internal/lib';
import { useEffect, useRef } from 'react';
import { capitalize } from '../lib/capitalize';
import { useOsInfo } from './useOsInfo';
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
@@ -176,13 +175,12 @@ export function useHotKeyLabel(action: HotkeyAction): string {
}
export function useFormattedHotkey(action: HotkeyAction | null): string[] | null {
const osInfo = useOsInfo();
const trigger = action != null ? (hotkeys[action]?.[0] ?? null) : null;
if (trigger == null || osInfo == null) {
if (trigger == null) {
return null;
}
const os = osInfo.osType;
const os = type();
const parts = trigger.split('+');
const labelParts: string[] = [];

View File

@@ -1,5 +0,0 @@
import { type } from '@tauri-apps/plugin-os';
export function useOsInfo() {
return { osType: type() };
}

View File

@@ -1,9 +1,8 @@
import { type } from '@tauri-apps/plugin-os';
import { useIsFullscreen } from './useIsFullscreen';
import { useOsInfo } from './useOsInfo';
export function useStoplightsVisible() {
const platform = useOsInfo();
const fullscreen = useIsFullscreen();
const stoplightsVisible = platform?.osType === 'macos' && !fullscreen;
const stoplightsVisible = type() === 'macos' && !fullscreen;
return stoplightsVisible;
}

View File

@@ -1,5 +1,5 @@
export const HEADER_SIZE_MD = '27px';
export const HEADER_SIZE_LG = '38px';
export const HEADER_SIZE_LG = '40px';
export const WINDOW_CONTROLS_WIDTH = '10.5rem';

View File

@@ -11,9 +11,9 @@ import { Dialogs } from '../components/Dialogs';
import { GlobalHooks } from '../components/GlobalHooks';
import RouteError from '../components/RouteError';
import { Toasts } from '../components/Toasts';
import { useOsInfo } from '../hooks/useOsInfo';
import { jotaiStore } from '../lib/jotai';
import { queryClient } from '../lib/queryClient';
import { type } from '@tauri-apps/plugin-os';
export const Route = createRootRoute({
component: RouteComponent,
@@ -21,7 +21,6 @@ export const Route = createRootRoute({
});
function RouteComponent() {
const osInfo = useOsInfo();
return (
<JotaiProvider store={jotaiStore}>
<QueryClientProvider client={queryClient}>
@@ -36,7 +35,7 @@ function RouteComponent() {
<div
className={classNames(
'w-full h-full',
osInfo?.osType === 'linux' && 'border border-border-subtle',
type() === 'linux' && 'border border-border-subtle',
)}
>
<Outlet />