Files
pureref-tauri/src/components/TitleBar.vue
小煜 f5b8a563cb feat: 初始化项目基础架构和核心功能
添加项目基础文件结构,包括Tauri配置、前端框架和核心功能模块
实现WebGPU渲染器基础、纹理管理和着色器
添加UI组件包括标题栏、确认对话框、帮助窗口等
设置状态管理存储和工具函数
配置构建工具和开发环境
2025-12-19 16:22:27 +08:00

257 lines
6.1 KiB
Vue

<script setup lang="ts">
/**
* TitleBar Component
* Shows when mouse hovers near the top of the window
*/
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps<{
title?: string;
alwaysOnTop?: boolean;
isDirty?: boolean;
hasContent?: boolean;
}>();
const emit = defineEmits<{
(e: 'minimize'): void;
(e: 'maximize'): void;
(e: 'close'): void;
(e: 'toggle-pin'): void;
(e: 'start-drag'): void;
}>();
const isVisible = ref(false);
const isDragging = ref(false);
const isMouseInTitleBar = ref(false);
// Show title bar when mouse is near top of window
const TRIGGER_ZONE = 30; // pixels from top to trigger
const TITLE_BAR_HEIGHT = 32; // height of title bar
const HIDE_DELAY = 300; // ms delay before hiding
let hideTimeout: number | null = null;
function handleMouseMove(e: MouseEvent) {
// Show if mouse is in trigger zone
if (e.clientY <= TRIGGER_ZONE) {
showTitleBar();
} else if (isVisible.value && !isMouseInTitleBar.value && !isDragging.value) {
// Hide if mouse moved away from both trigger zone and title bar
if (e.clientY > TITLE_BAR_HEIGHT) {
scheduleHide();
}
}
}
function showTitleBar() {
cancelHide();
isVisible.value = true;
}
function scheduleHide() {
// Don't hide while dragging
if (isDragging.value) return;
if (hideTimeout === null) {
hideTimeout = window.setTimeout(() => {
if (!isMouseInTitleBar.value && !isDragging.value) {
isVisible.value = false;
}
hideTimeout = null;
}, HIDE_DELAY);
}
}
function cancelHide() {
if (hideTimeout !== null) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
}
function handleTitleBarMouseEnter() {
isMouseInTitleBar.value = true;
cancelHide();
isVisible.value = true;
}
function handleTitleBarMouseLeave() {
isMouseInTitleBar.value = false;
// Don't hide while dragging
if (!isDragging.value) {
scheduleHide();
}
}
function handleDoubleClick() {
emit('maximize');
}
async function handleDragStart(e: MouseEvent) {
if (e.button === 0) {
isDragging.value = true;
emit('start-drag');
// Listen for mouseup to end dragging
const handleMouseUp = (upEvent: MouseEvent) => {
isDragging.value = false;
window.removeEventListener('mouseup', handleMouseUp);
// Check if mouse is outside title bar area after drag ends
if (upEvent.clientY > TITLE_BAR_HEIGHT) {
isMouseInTitleBar.value = false;
scheduleHide();
}
};
window.addEventListener('mouseup', handleMouseUp);
}
}
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove);
});
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove);
if (hideTimeout) {
clearTimeout(hideTimeout);
}
});
</script>
<template>
<div
ref="titleBarRef"
class="title-bar"
:class="{ 'title-bar--visible': isVisible }"
@mouseenter="handleTitleBarMouseEnter"
@mouseleave="handleTitleBarMouseLeave"
>
<div
class="title-bar__drag-area"
@dblclick="handleDoubleClick"
@mousedown="handleDragStart"
>
<span class="title-bar__title">
<template v-if="hasContent">{{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}</template>
</span>
</div>
<div class="title-bar__buttons">
<!-- Pin icon: mdi:pin -->
<button
class="title-bar__btn title-bar__btn--pin"
:class="{ 'title-bar__btn--active': alwaysOnTop }"
@click="emit('toggle-pin')"
title="置顶"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2z"/>
</svg>
</button>
<!-- Minimize icon: mdi:minus -->
<button
class="title-bar__btn title-bar__btn--minimize"
@click="emit('minimize')"
title="最小化"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 13H5v-2h14z"/>
</svg>
</button>
<!-- Maximize icon: mdi:checkbox-blank-outline -->
<button
class="title-bar__btn title-bar__btn--maximize"
@click="emit('maximize')"
title="最大化"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14z"/>
</svg>
</button>
<!-- Close icon: mdi:close -->
<button
class="title-bar__btn title-bar__btn--close"
@click="emit('close')"
title="关闭"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/>
</svg>
</button>
</div>
</div>
</template>
<style scoped>
.title-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 32px;
background: rgba(30, 30, 30, 0.95);
border-bottom: 1px solid #000;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
transform: translateY(-100%);
transition: transform 0.2s ease-out;
-webkit-app-region: no-drag;
}
.title-bar--visible {
transform: translateY(0);
}
.title-bar__drag-area {
flex: 1;
height: 100%;
display: flex;
align-items: center;
padding-left: 12px;
-webkit-app-region: drag;
cursor: default;
}
.title-bar__title {
color: #ccc;
font-size: 13px;
user-select: none;
}
.title-bar__buttons {
display: flex;
height: 100%;
-webkit-app-region: no-drag;
}
.title-bar__btn {
width: 46px;
height: 100%;
border: none;
background: transparent;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s, color 0.15s;
}
.title-bar__btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.title-bar__btn--pin.title-bar__btn--active {
color: #00c8ff;
}
.title-bar__btn--close:hover {
background: #e81123;
color: #fff;
}
</style>