自定义装饰栏功能

This commit is contained in:
2026-07-08 14:53:41 +08:00
parent a7bcd03534
commit 0c8d5d1624
2 changed files with 232 additions and 13 deletions

View File

@@ -5,6 +5,10 @@
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-start-dragging",
"opener:default", "opener:default",
"dialog:default" "dialog:default"
] ]

View File

@@ -2,6 +2,7 @@
import {computed, ref, watch, onMounted, onUnmounted} from "vue"; import {computed, ref, watch, onMounted, onUnmounted} from "vue";
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event"; import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
import {invoke, isTauri} from "@tauri-apps/api/core"; import {invoke, isTauri} from "@tauri-apps/api/core";
import {getCurrentWindow} from "@tauri-apps/api/window";
import MarkdownEditor from "./components/MarkdownEditor.vue"; import MarkdownEditor from "./components/MarkdownEditor.vue";
import DirectorySidebar from "./components/DirectorySidebar.vue"; import DirectorySidebar from "./components/DirectorySidebar.vue";
import OutlinePanel from "./components/OutlinePanel.vue"; import OutlinePanel from "./components/OutlinePanel.vue";
@@ -135,10 +136,105 @@ const showMoreMenu = ref(false);
const settingsRequest = ref(0); const settingsRequest = ref(0);
const settingsInitialPage = ref<"about" | "editor">("about"); const settingsInitialPage = ref<"about" | "editor">("about");
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null); const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
const isWindowMaximized = ref(false);
const showStartActions = computed(() => { const showStartActions = computed(() => {
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value; return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
}); });
type TitlebarDragState = {
startX: number;
startY: number;
};
const TITLEBAR_DRAG_THRESHOLD = 4;
let titlebarDragState: TitlebarDragState | null = null;
let unlistenWindowResize: (() => void) | null = null;
function isInteractiveTitlebarTarget(target: EventTarget | null) {
if (!(target instanceof Element)) return false;
return Boolean(target.closest("button, a, input, textarea, select, [data-no-window-drag]"));
}
async function refreshWindowMaximized() {
if (!isTauri()) return;
try {
isWindowMaximized.value = await getCurrentWindow().isMaximized();
} catch (error) {
console.error("Failed to read window state:", error);
}
}
async function minimizeWindow() {
if (!isTauri()) return;
try {
await getCurrentWindow().minimize();
} catch (error) {
console.error("Failed to minimize window:", error);
}
}
async function toggleWindowMaximize() {
if (!isTauri()) return;
try {
const appWindow = getCurrentWindow();
await appWindow.toggleMaximize();
isWindowMaximized.value = await appWindow.isMaximized();
} catch (error) {
console.error("Failed to toggle window maximize:", error);
}
}
async function closeWindow() {
if (!isTauri()) return;
try {
await getCurrentWindow().close();
} catch (error) {
console.error("Failed to close window:", error);
}
}
function removeTitlebarDragListeners() {
window.removeEventListener("mousemove", onTitlebarMouseMove);
window.removeEventListener("mouseup", removeTitlebarDragListeners);
titlebarDragState = null;
}
function onTitlebarMouseDown(event: MouseEvent) {
if (event.button !== 0 || event.detail > 1 || isInteractiveTitlebarTarget(event.target)) return;
event.preventDefault();
titlebarDragState = {
startX: event.screenX,
startY: event.screenY,
};
window.addEventListener("mousemove", onTitlebarMouseMove);
window.addEventListener("mouseup", removeTitlebarDragListeners, {once: true});
}
function onTitlebarMouseMove(event: MouseEvent) {
if (!titlebarDragState) return;
const movedX = Math.abs(event.screenX - titlebarDragState.startX);
const movedY = Math.abs(event.screenY - titlebarDragState.startY);
if (movedX < TITLEBAR_DRAG_THRESHOLD && movedY < TITLEBAR_DRAG_THRESHOLD) return;
event.preventDefault();
removeTitlebarDragListeners();
if (!isTauri()) return;
void getCurrentWindow().startDragging().catch((error) => {
console.error("Failed to start window drag:", error);
});
}
function onTitlebarDoubleClick(event: MouseEvent) {
if (isInteractiveTitlebarTarget(event.target)) return;
event.preventDefault();
removeTitlebarDragListeners();
void toggleWindowMaximize();
}
function openSettings(page: "about" | "editor" = "about") { function openSettings(page: "about" | "editor" = "about") {
settingsInitialPage.value = page; settingsInitialPage.value = page;
settingsRequest.value += 1; settingsRequest.value += 1;
@@ -181,11 +277,26 @@ onMounted(() => {
applyAppTheme(appTheme.value); applyAppTheme(appTheme.value);
applyZoomCssVars(); applyZoomCssVars();
window.addEventListener("wheel", onEditorWheel, {passive: false}); window.addEventListener("wheel", onEditorWheel, {passive: false});
if (isTauri()) {
void refreshWindowMaximized();
void getCurrentWindow().onResized(() => {
void refreshWindowMaximized();
}).then((unlisten) => {
unlistenWindowResize = unlisten;
}).catch((error) => {
console.error("Failed to listen for window resize:", error);
});
}
void restoreLastWorkspace(); void restoreLastWorkspace();
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("wheel", onEditorWheel); window.removeEventListener("wheel", onEditorWheel);
removeTitlebarDragListeners();
if (unlistenWindowResize) {
unlistenWindowResize();
unlistenWindowResize = null;
}
if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer); if (zoomIndicatorTimer) clearTimeout(zoomIndicatorTimer);
}); });
@@ -478,28 +589,45 @@ registerTauriListener("menu-event", (event) => {
<!-- Right: Editor area --> <!-- Right: Editor area -->
<main class="flex-1 flex flex-col overflow-hidden relative"> <main class="flex-1 flex flex-col overflow-hidden relative">
<!-- Toolbar --> <!-- Custom title bar / toolbar -->
<div <div
v-if="!showStartActions" class="editor-titlebar flex w-full items-center h-10 pl-3 pr-0 select-none shrink-0"
class="editor-toolbar flex w-full items-center h-10 px-3 select-none shrink-0" @mousedown="onTitlebarMouseDown"
@dblclick="onTitlebarDoubleClick"
> >
<!-- Left: Outline toggle --> <!-- Left: Outline toggle -->
<div
v-if="!showStartActions"
class="flex items-center shrink-0"
data-no-window-drag
>
<button <button
class="toolbar-btn" class="toolbar-btn"
:class="{ 'toolbar-btn-active': showOutline }" :class="{ 'toolbar-btn-active': showOutline }"
title="切换大纲" title="切换大纲"
aria-label="切换大纲"
@click="showOutline = !showOutline" @click="showOutline = !showOutline"
> >
<i class="ri-menu-line text-base"></i> <i class="ri-menu-line text-base"></i>
</button> </button>
</div>
<div v-else class="titlebar-brand app-muted-text shrink-0 pointer-events-none">
yurou
</div>
<!-- Center: Filename --> <!-- Center: Filename -->
<div class="app-muted-text flex-1 text-center text-sm font-medium truncate px-3 select-none"> <div class="titlebar-drag-surface flex-1 min-w-0 px-3">
<div class="app-muted-text titlebar-file-title text-center text-sm font-medium truncate pointer-events-none">
{{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }} {{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
</div> </div>
</div>
<!-- Right: Actions --> <!-- Right: Actions -->
<div v-if="!showStartActions" class="flex items-center gap-0.5"> <div
v-if="!showStartActions"
class="flex items-center gap-0.5 shrink-0"
data-no-window-drag
>
<!-- Document mode --> <!-- Document mode -->
<div class="mode-toggle" role="group" aria-label="文档模式"> <div class="mode-toggle" role="group" aria-label="文档模式">
<button <button
@@ -557,6 +685,34 @@ registerTauriListener("menu-event", (event) => {
</div> </div>
</div> </div>
</div> </div>
<!-- Window controls -->
<div class="window-controls flex items-stretch self-stretch ml-2 shrink-0" data-no-window-drag>
<button
class="window-control-btn"
title="最小化"
aria-label="最小化"
@click="minimizeWindow"
>
<i class="ri-subtract-line"></i>
</button>
<button
class="window-control-btn"
:title="isWindowMaximized ? '还原' : '最大化'"
:aria-label="isWindowMaximized ? '还原' : '最大化'"
@click="toggleWindowMaximize"
>
<i :class="isWindowMaximized ? 'ri-checkbox-multiple-blank-line' : 'ri-checkbox-blank-line'"></i>
</button>
<button
class="window-control-btn window-control-close"
title="关闭"
aria-label="关闭"
@click="closeWindow"
>
<i class="ri-close-line"></i>
</button>
</div>
</div> </div>
<div class="flex-1 flex min-h-0 overflow-hidden"> <div class="flex-1 flex min-h-0 overflow-hidden">
<Transition name="outline-slide"> <Transition name="outline-slide">
@@ -657,7 +813,7 @@ registerTauriListener("menu-event", (event) => {
color: var(--app-text); color: var(--app-text);
} }
.editor-toolbar { .editor-titlebar {
background: var(--app-bg); background: var(--app-bg);
border-bottom: 1px solid var(--app-border-strong); border-bottom: 1px solid var(--app-border-strong);
} }
@@ -686,6 +842,28 @@ registerTauriListener("menu-event", (event) => {
/* ── Toolbar ───────────────────────────────────────────────────── */ /* ── Toolbar ───────────────────────────────────────────────────── */
.titlebar-drag-surface {
height: 100%;
min-width: 32px;
display: flex;
align-items: center;
}
.titlebar-brand {
height: 100%;
min-width: 68px;
display: inline-flex;
align-items: center;
font-size: 12px;
font-weight: 600;
letter-spacing: 0;
}
.titlebar-file-title {
width: 100%;
line-height: 1.2;
}
.toolbar-btn { .toolbar-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -711,6 +889,43 @@ registerTauriListener("menu-event", (event) => {
background-color: var(--app-surface-muted); background-color: var(--app-surface-muted);
} }
.window-controls {
color: var(--app-muted);
}
.window-control-btn {
width: 46px;
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
font-size: 15px;
line-height: 1;
transition:
background-color 0.15s ease,
color 0.15s ease;
}
.window-control-btn:hover {
background-color: var(--app-hover);
color: var(--app-text);
}
.window-control-btn:focus-visible {
outline: 1px solid var(--app-accent);
outline-offset: -3px;
}
.window-control-close:hover {
background-color: #c42b1c;
color: #ffffff;
}
.mode-toggle { .mode-toggle {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;