mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
1
Commits
files-grid-perf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbc8af1951 |
@@ -6,6 +6,12 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-internal-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-start-dragging",
|
||||
"http:default",
|
||||
{
|
||||
"identifier": "http:allow-fetch",
|
||||
|
||||
@@ -48,8 +48,11 @@ fn build_window(app: &AppHandle, label: &str, url: &str) -> Result<WebviewWindow
|
||||
// dir (and thus IndexedDB / localStorage / cookies). macOS (WKWebView) and
|
||||
// Linux (WebKitGTK) don't have this constraint, so the arg is Windows-only.
|
||||
#[cfg(target_os = "windows")]
|
||||
let builder =
|
||||
builder.additional_browser_args("--enable-features=CertVerifierBuiltinFeature");
|
||||
let builder = builder
|
||||
.additional_browser_args("--enable-features=CertVerifierBuiltinFeature")
|
||||
// Windows: no native title bar; the frontend draws its own (WindowTitleBar).
|
||||
// macOS/Linux keep native decorations.
|
||||
.decorations(false);
|
||||
|
||||
builder.build().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -146,6 +146,17 @@ pub fn run() {
|
||||
.setup(|app| {
|
||||
add_log("🚀 Tauri app setup started".to_string());
|
||||
|
||||
// Windows: drop the native title bar so the in-app custom title bar
|
||||
// (window controls + drag region) takes over. Runtime toggle because the
|
||||
// main window is defined in tauri.conf.json; spawned windows set it at
|
||||
// build time in window.rs. macOS/Linux keep their native decorations.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
||||
let _ = window.set_decorations(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Files passed on the command line at first launch load into the main
|
||||
// window once the frontend mounts.
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
|
||||
import { DesktopConfigSync } from "@app/components/DesktopConfigSync";
|
||||
import { WindowTitleBar } from "@app/components/WindowTitleBar";
|
||||
import { DesktopQueryCacheReset } from "@app/components/DesktopQueryCacheReset";
|
||||
import { DesktopBannerInitializer } from "@app/components/DesktopBannerInitializer";
|
||||
import { SaveShortcutListener } from "@app/components/SaveShortcutListener";
|
||||
@@ -328,6 +329,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
>
|
||||
{/* Also here: the auth check below switches mode pre-authChecked. */}
|
||||
<DesktopQueryCacheReset />
|
||||
<WindowTitleBar />
|
||||
<div style={{ minHeight: "100vh" }} />
|
||||
{updatePopupModal}
|
||||
</ProprietaryAppProviders>
|
||||
@@ -354,6 +356,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
}}
|
||||
>
|
||||
<DesktopQueryCacheReset />
|
||||
<WindowTitleBar />
|
||||
<SaaSTeamProvider key={appKey}>
|
||||
<DesktopConfigSync />
|
||||
<DesktopBannerInitializer />
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Custom Windows window controls (see WindowTitleBar.tsx): a fixed cluster in
|
||||
the top-right corner, overlaying the app chrome — which reserves the corner
|
||||
via --wincontrols-w. Kept above app overlays so the controls stay usable. */
|
||||
.titleBar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--c-text-muted);
|
||||
font-size: 15px; /* drives the inherit-sized MUI icons */
|
||||
line-height: 1;
|
||||
cursor: default;
|
||||
transition:
|
||||
background-color 0.12s ease,
|
||||
color 0.12s ease;
|
||||
}
|
||||
|
||||
/* Clicks (and Tauri's drag-region hit test) should always resolve to the
|
||||
button, never its SVG glyph. */
|
||||
.button svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--c-hover);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
background: var(--c-danger);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.restoreIcon {
|
||||
/* The overlapping-squares glyph reads large next to the others. */
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsomorphicEffect } from "@mantine/hooks";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import MinimizeIcon from "@mui/icons-material/Minimize";
|
||||
import CropSquareIcon from "@mui/icons-material/CropSquare";
|
||||
import FilterNoneIcon from "@mui/icons-material/FilterNone";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { getDesktopOs, DesktopOs } from "@app/services/platformService";
|
||||
import styles from "@app/components/WindowTitleBar.module.css";
|
||||
// Desktop-only skin that reserves the controls' corner across core layout
|
||||
// surfaces; every rule is gated by the data-window-controls flag set below.
|
||||
import "@app/components/windowChrome.css";
|
||||
|
||||
// Seed from the UA so the bar (and its reserved height) is present on the first
|
||||
// frame on Windows, avoiding a layout shift. getDesktopOs() confirms it right
|
||||
// after via the Rust command.
|
||||
const seedIsWindows =
|
||||
typeof navigator !== "undefined" && /Windows/i.test(navigator.userAgent);
|
||||
|
||||
/**
|
||||
* Custom window controls for the Windows desktop build. The native caption is
|
||||
* removed in Rust (decorations:false), so this draws minimize/maximize/close as
|
||||
* a fixed overlay pinned to the top-right corner — the rail and panels run all
|
||||
* the way to the window edge, and the app chrome reserves the corner via the
|
||||
* data-window-controls flag this sets on <html> (consumed by windowChrome.css).
|
||||
* Renders nothing on macOS/Linux (native decorations kept); not bundled in the
|
||||
* browser build. tao provides edge/corner resize for the undecorated window, so
|
||||
* no manual resize handles are needed here.
|
||||
*/
|
||||
export function WindowTitleBar() {
|
||||
const [isWindows, setIsWindows] = useState(seedIsWindows);
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
const active = isWindows && isTauri();
|
||||
|
||||
// Confirm the OS authoritatively (the UA seed is only a first-frame guess).
|
||||
useEffect(() => {
|
||||
if (!isTauri()) {
|
||||
setIsWindows(false);
|
||||
return;
|
||||
}
|
||||
let mounted = true;
|
||||
void getDesktopOs().then((os) => {
|
||||
if (mounted) setIsWindows(os === DesktopOs.Windows);
|
||||
});
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Flag the custom chrome on <html> so windowChrome.css can reserve the
|
||||
// controls' corner across the app. Set only when active (Windows desktop);
|
||||
// absent otherwise, so the skin is inert on macOS/Linux. Layout effect so it
|
||||
// lands before paint.
|
||||
useIsomorphicEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (active) {
|
||||
root.setAttribute("data-window-controls", "custom");
|
||||
} else {
|
||||
root.removeAttribute("data-window-controls");
|
||||
}
|
||||
return () => {
|
||||
root.removeAttribute("data-window-controls");
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
// Keep the maximize/restore icon in sync with the actual window state
|
||||
// (double-click, snap, or the button itself all change it).
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const appWindow = getCurrentWindow();
|
||||
let unlisten: (() => void) | undefined;
|
||||
void appWindow.isMaximized().then(setMaximized);
|
||||
void appWindow
|
||||
.onResized(() => {
|
||||
void appWindow.isMaximized().then(setMaximized);
|
||||
})
|
||||
.then((u) => {
|
||||
unlisten = u;
|
||||
});
|
||||
return () => unlisten?.();
|
||||
}, [active]);
|
||||
|
||||
// Let the window be dragged, and double-click-maximized, from any
|
||||
// non-interactive spot in the top strip. data-tauri-drag-region only fires on
|
||||
// bare container backgrounds, leaving most of a busy toolbar undraggable, so a
|
||||
// document-level hit test covers the whole top instead.
|
||||
//
|
||||
// startDragging() must NOT run on mousedown: it enters the OS drag loop and
|
||||
// swallows the browser's dblclick. So begin the drag on the first real move,
|
||||
// and detect a double-click from the interval between mousedowns.
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const TOP_STRIP_PX = 48;
|
||||
const DOUBLE_CLICK_MS = 500;
|
||||
const DRAG_THRESHOLD_PX = 4;
|
||||
const INTERACTIVE =
|
||||
"button, a[href], input, textarea, select, label, summary," +
|
||||
'[role="button"], [role="tab"], [role="menuitem"], [role="switch"],' +
|
||||
'[role="slider"], [contenteditable="true"], [data-no-window-drag]';
|
||||
const draggableAt = (e: MouseEvent) => {
|
||||
if (e.button !== 0 || e.clientY > TOP_STRIP_PX) return false;
|
||||
const el = e.target as Element | null;
|
||||
return !!el && !el.closest(INTERACTIVE);
|
||||
};
|
||||
let pending: { x: number; y: number } | null = null;
|
||||
let lastDownAt = 0;
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!draggableAt(e)) {
|
||||
pending = null;
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - lastDownAt < DOUBLE_CLICK_MS) {
|
||||
pending = null;
|
||||
lastDownAt = 0;
|
||||
void getCurrentWindow().toggleMaximize();
|
||||
return;
|
||||
}
|
||||
lastDownAt = now;
|
||||
pending = { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!pending) return;
|
||||
if (
|
||||
Math.abs(e.clientX - pending.x) > DRAG_THRESHOLD_PX ||
|
||||
Math.abs(e.clientY - pending.y) > DRAG_THRESHOLD_PX
|
||||
) {
|
||||
pending = null;
|
||||
lastDownAt = 0; // a drag is not the first half of a double-click
|
||||
void getCurrentWindow().startDragging();
|
||||
}
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
pending = null;
|
||||
};
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
return (
|
||||
<div className={styles.titleBar}>
|
||||
<div className={styles.controls}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
onClick={() => void appWindow.minimize()}
|
||||
aria-label="Minimize"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<MinimizeIcon fontSize="inherit" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.button}
|
||||
onClick={() => void appWindow.toggleMaximize()}
|
||||
aria-label={maximized ? "Restore" : "Maximize"}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{maximized ? (
|
||||
<FilterNoneIcon fontSize="inherit" className={styles.restoreIcon} />
|
||||
) : (
|
||||
<CropSquareIcon fontSize="inherit" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.button} ${styles.close}`}
|
||||
onClick={() => void appWindow.close()}
|
||||
aria-label="Close"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<CloseIcon fontSize="inherit" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* Desktop (Windows) custom window chrome.
|
||||
*
|
||||
* The window controls are a fixed overlay in the top-right corner (see
|
||||
* WindowTitleBar). These rules keep the app's own chrome clear of that corner.
|
||||
*
|
||||
* Loaded only in the desktop build (imported by WindowTitleBar) and gated on the
|
||||
* data-window-controls flag the overlay sets on <html> only when active, so it
|
||||
* is inert on macOS/Linux (flag never set) and absent from web builds. It
|
||||
* targets core layout class names on purpose: this is the single, contained
|
||||
* coupling point between the desktop chrome and the core layout, kept here in
|
||||
* the desktop layer so the core components stay unaware of window chrome. */
|
||||
|
||||
html[data-window-controls="custom"] {
|
||||
--wincontrols-w: 8.625rem; /* three 46px controls */
|
||||
--wincontrols-h: 2rem; /* control height */
|
||||
}
|
||||
|
||||
/* Files-page toolbar spans the width; reflow search + actions left of the
|
||||
controls by shrinking the grid's right edge. */
|
||||
html[data-window-controls="custom"] .files-page-header {
|
||||
padding-right: calc(0.75rem + var(--wincontrols-w));
|
||||
}
|
||||
|
||||
/* Right panel, expanded: move the header (PDF Tools, or an active tool such as
|
||||
Automate) below the controls instead of squashing its title. */
|
||||
html[data-window-controls="custom"] .tool-panel__compact-header,
|
||||
html[data-window-controls="custom"] .tool-panel .sui-panelhdr {
|
||||
margin-top: var(--wincontrols-h);
|
||||
}
|
||||
|
||||
/* Right panel, collapsed: the strip is narrower than the controls, so push its
|
||||
expand toggle straight down. */
|
||||
html[data-window-controls="custom"] .tool-panel__collapsed-strip {
|
||||
padding-top: calc(10px + var(--wincontrols-h));
|
||||
}
|
||||
|
||||
/* Viewer top bar: when the right panel is collapsed the bar widens under the
|
||||
controls. :has() detects the collapsed strip (no flag needed in core) and we
|
||||
push the right cluster clear. --nav-rail-w is the collapsed strip's width, so
|
||||
only the controls' overhang past it is reserved. */
|
||||
html[data-window-controls="custom"]
|
||||
.app-frame__content:has(.tool-panel__collapsed-strip)
|
||||
.workbench-bar-globals {
|
||||
margin-right: calc(var(--wincontrols-w) - var(--nav-rail-w));
|
||||
}
|
||||
|
||||
/* Mobile layout: the top bar spans the full width. Keep the brand top-left
|
||||
(already clear of the top-right controls) and drop the view switcher below
|
||||
them by top-aligning the row and nudging the switcher down. */
|
||||
html[data-window-controls="custom"] .mobile-toggle {
|
||||
align-items: flex-start;
|
||||
}
|
||||
html[data-window-controls="custom"] .mobile-toggle-buttons {
|
||||
margin-top: var(--wincontrols-h);
|
||||
}
|
||||
Reference in New Issue
Block a user