Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/tauri/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ cargo check --locked
cargo test --locked
```

## Window chrome

- **Windows / Linux:** undecorated; HTML title bar draws Fluent-style SVG caption buttons (min / max-restore / close).
- **macOS:** `decorations: true` + `titleBarStyle: Overlay` / `hiddenTitle` / `trafficLightPosition` so **system traffic lights** sit in the custom bar. Frontend reserves `--q-mac-traffic-inset` (no fake CSS lights; same idea as Flutter `useNativeWindowChrome`).

Main window: `"create": false` in `tauri.conf.json`, opened from Rust (`open_main_window`) so macOS can enable decorations without affecting Win/Linux.

## Layout

```
Expand Down
26 changes: 26 additions & 0 deletions apps/tauri/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ fn host_platform() -> String {
}
}

/// Build the main window from `tauri.conf.json`, applying OS chrome rules.
///
/// - **macOS:** native traffic lights via `titleBarStyle: Overlay` + `hiddenTitle`
/// (decorations stay on). Custom HTML title bar sits under the system controls.
/// - **Windows / Linux:** undecorated; frontend draws Fluent-style caption buttons.
fn open_main_window(app: &tauri::App) -> tauri::Result<()> {
use tauri::WebviewWindowBuilder;

let mut cfg = app
.config()
.app
.windows
.first()
.cloned()
.ok_or_else(|| tauri::Error::WindowNotFound)?;

// Overlay + hiddenTitle + trafficLightPosition stay in tauri.conf.json (macOS).
cfg.decorations = cfg!(target_os = "macos");

WebviewWindowBuilder::from_config(app.handle(), &cfg)?.build()?;
Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All @@ -52,6 +75,9 @@ pub fn run() {
std::io::Error::other(format!("open profile store: {e}"))
})?;
app.manage(store);
open_main_window(app).map_err(|e| {
std::io::Error::other(format!("open main window: {e}"))
})?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
Expand Down
10 changes: 9 additions & 1 deletion apps/tauri/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@
"app": {
"windows": [
{
"label": "main",
"create": false,
"title": "Querya",
"width": 1280,
"height": 800,
"minWidth": 900,
"minHeight": 600,
"decorations": false,
"resizable": true,
"fullscreen": false
"fullscreen": false,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"trafficLightPosition": {
"x": 16,
"y": 13
}
}
],
"security": {
Expand Down
2 changes: 2 additions & 0 deletions apps/tauri/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
--q-control-h-md: 32px;
--q-control-h-lg: 36px;
--q-titlebar-h: 40px;
/* Space reserved for macOS system traffic lights under Overlay titlebar. */
--q-mac-traffic-inset: 78px;
--q-statusbar-h: 26px;
--q-split-handle: 6px;
--q-sidebar-min: 180px;
Expand Down
164 changes: 82 additions & 82 deletions apps/tauri/src/ui/TitleBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
import {
detectPlatformFromUa,
resolvePlatform,
usesNativeTrafficLights,
windowControlsSide,
type QueryaPlatform,
} from "./platform";
Expand Down Expand Up @@ -33,15 +34,23 @@ const shell = useShellStore();
const platform = ref<QueryaPlatform>(detectPlatformFromUa());
const maximized = ref(false);

/** macOS: left traffic lights. Win/Linux: right caption buttons. */
/** macOS: left (native). Win/Linux: right caption buttons. */
const controlsSide = computed(() => windowControlsSide(platform.value));
const controlsOnLeft = computed(() => controlsSide.value === "left");
const nativeTrafficLights = computed(() =>
usesNativeTrafficLights(platform.value),
);

const barStyle = computed(() => ({
transitionDuration: `${motion.duration("fast")}ms`,
transitionTimingFunction: motion.easing("standard"),
}));

function isChromeInteractive(target: EventTarget | null): boolean {
return Boolean(
(target as HTMLElement | null)?.closest("button, a, input, [data-no-drag]"),
);
}

async function refreshMaximized() {
try {
maximized.value = await getCurrentWindow().isMaximized();
Expand All @@ -64,9 +73,7 @@ async function onClose() {
}

async function onDrag(e: MouseEvent) {
if (e.button !== 0) return;
const target = e.target as HTMLElement | null;
if (target?.closest("button, a, input, [data-no-drag]")) return;
if (e.button !== 0 || isChromeInteractive(e.target)) return;
try {
await getCurrentWindow().startDragging();
} catch {
Expand All @@ -75,8 +82,7 @@ async function onDrag(e: MouseEvent) {
}

function onDblClick(e: MouseEvent) {
const target = e.target as HTMLElement | null;
if (target?.closest("button, a, input, [data-no-drag]")) return;
if (isChromeInteractive(e.target)) return;
void onToggleMaximize();
}

Expand Down Expand Up @@ -105,32 +111,19 @@ onUnmounted(() => {
data-testid="title-bar"
:data-platform="platform"
:data-controls="controlsSide"
:data-native-traffic="String(nativeTrafficLights)"
:style="barStyle"
@mousedown="onDrag"
@dblclick="onDblClick"
>
<!-- Always three regions so Win/Linux keep controls pinned right. -->
<!-- Three regions keep Win/Linux caption buttons pinned right. -->
<div class="region region--start" data-no-drag>
<div v-if="controlsOnLeft" class="controls controls--traffic">
<button
type="button"
class="traffic close"
aria-label="Close"
@click="onClose"
/>
<button
type="button"
class="traffic minimize"
aria-label="Minimize"
@click="onMinimize"
/>
<button
type="button"
class="traffic maximize"
:aria-label="maximized ? 'Restore' : 'Maximize'"
@click="onToggleMaximize"
/>
</div>
<!-- macOS Overlay: reserve inset so sidebar/brand clear system lights. -->
<span
v-if="nativeTrafficLights"
class="mac-inset"
aria-hidden="true"
/>
<button
type="button"
class="sidebar-toggle"
Expand Down Expand Up @@ -160,40 +153,64 @@ onUnmounted(() => {
<div class="region region--center" data-tauri-drag-region />

<div class="region region--end" data-no-drag>
<div v-if="!controlsOnLeft" class="controls controls--caption">
<div v-if="!nativeTrafficLights" class="controls controls--caption">
<button
type="button"
class="caption minimize"
aria-label="Minimize"
@click="onMinimize"
>
<span class="glyph" aria-hidden="true">─</span>
<svg class="glyph" viewBox="0 0 10 10" width="10" height="10" aria-hidden="true">
<path d="M1 5h8" />
</svg>
</button>
<button
type="button"
class="caption maximize"
:aria-label="maximized ? 'Restore' : 'Maximize'"
@click="onToggleMaximize"
>
<span class="glyph" aria-hidden="true">{{
maximized ? "❐" : "□"
}}</span>
<!-- Restore: overlapping squares (Win 10/11). Maximize: single square. -->
<svg
v-if="maximized"
class="glyph glyph--join"
viewBox="0 0 10 10"
width="10"
height="10"
aria-hidden="true"
>
<path d="M3 2.5h4.5V7M2.5 3.5H7v4.5H2.5z" />
</svg>
<svg
v-else
class="glyph"
viewBox="0 0 10 10"
width="10"
height="10"
aria-hidden="true"
>
<rect x="1.5" y="1.5" width="7" height="7" />
</svg>
</button>
<button
type="button"
class="caption close"
aria-label="Close"
@click="onClose"
>
<span class="glyph" aria-hidden="true">✕</span>
<svg
class="glyph glyph--close"
viewBox="0 0 10 10"
width="10"
height="10"
aria-hidden="true"
>
<path d="M2 2l6 6M8 2l-6 6" />
</svg>
</button>
</div>
<!-- Balance macOS traffic-light width so the title stays centered. -->
<div
v-else
class="mac-balance"
aria-hidden="true"
/>
<!-- Balance the macOS traffic-light inset so the drag column stays even. -->
<span v-else class="mac-inset" aria-hidden="true" />
</div>
</header>
</template>
Expand Down Expand Up @@ -288,45 +305,17 @@ onUnmounted(() => {
flex: 0 0 auto;
}

/* macOS traffic lights — close / minimize / maximize */
.controls--traffic {
gap: 8px;
padding: 0 4px 0 6px;
min-width: 52px;
}

.traffic {
width: 12px;
height: 12px;
border-radius: 999px;
border: 0;
padding: 0;
cursor: default;
background: var(--q-accent);
}

.traffic.close {
background: #ff5f57;
}
.traffic.minimize {
background: #febc2e;
}
.traffic.maximize {
background: #28c840;
}

.traffic:hover {
filter: brightness(1.06);
}

.mac-balance {
/* traffic cluster (~52) + sidebar toggle (~28) + gaps ≈ start cluster */
width: 5.5rem;
/* Reserved width for system traffic lights (Overlay titlebar on macOS).
Native lights use window coords; counter-scale so UI zoom does not shrink the gap. */
.mac-inset {
display: inline-block;
width: calc(var(--q-mac-traffic-inset) / var(--q-ui-scale, 1));
height: 1px;
flex: 0 0 auto;
pointer-events: none;
}

/* Windows / Linux caption buttons — min / max / close on the right */
/* Windows / Linux — Fluent-style caption buttons (min / max / close). */
.controls--caption {
gap: 0;
height: 100%;
Expand All @@ -352,17 +341,28 @@ onUnmounted(() => {
}

.caption.close:hover {
background: var(--q-destructive);
color: var(--q-destructive-foreground);
background: #e81123;
color: #ffffff;
}

.glyph {
font-size: 11px;
line-height: 1;
display: block;
fill: none;
stroke: currentColor;
stroke-width: 1;
stroke-linecap: round;
}

.glyph--join {
stroke-linejoin: round;
}

.glyph--close {
stroke-width: 1.1;
}

/* Extra inset on macOS so lights clear the rounded corner / notch feel */
.titlebar[data-controls="left"] .region--start {
padding-left: 12px;
/* macOS Overlay: content starts after the system lights. */
.titlebar[data-native-traffic="true"] .region--start {
padding-left: 0;
}
</style>
30 changes: 30 additions & 0 deletions apps/tauri/src/ui/platform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import {
platformFromOsString,
usesNativeTrafficLights,
windowControlsSide,
} from "./platform";

describe("window chrome platform helpers", () => {
it("maps OS strings to platforms", () => {
expect(platformFromOsString("macos")).toBe("macos");
expect(platformFromOsString("darwin")).toBe("macos");
expect(platformFromOsString("windows")).toBe("windows");
expect(platformFromOsString("linux")).toBe("linux");
expect(platformFromOsString("freebsd")).toBe("unknown");
});

it("places controls left only on macOS", () => {
expect(windowControlsSide("macos")).toBe("left");
expect(windowControlsSide("windows")).toBe("right");
expect(windowControlsSide("linux")).toBe("right");
expect(windowControlsSide("unknown")).toBe("right");
});

it("uses native traffic lights only on macOS", () => {
expect(usesNativeTrafficLights("macos")).toBe(true);
expect(usesNativeTrafficLights("windows")).toBe(false);
expect(usesNativeTrafficLights("linux")).toBe(false);
expect(usesNativeTrafficLights("unknown")).toBe(false);
});
});
9 changes: 9 additions & 0 deletions apps/tauri/src/ui/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,19 @@ export function windowControlsSide(platform: QueryaPlatform): WindowControlsSide
return platform === "macos" ? "left" : "right";
}

/**
* macOS uses system traffic lights (`titleBarStyle: Overlay`).
* Win/Linux draw custom caption buttons in the HTML title bar.
*/
export function usesNativeTrafficLights(platform: QueryaPlatform): boolean {
return platform === "macos";
}

export function applyPlatformDataset(platform: QueryaPlatform): void {
const root = document.documentElement;
root.dataset.platform = platform;
root.dataset.windowControls = windowControlsSide(platform);
root.dataset.nativeTrafficLights = String(usesNativeTrafficLights(platform));
}

/** Resolve OS via Rust, fall back to UA when not in Tauri. */
Expand Down
Loading