knxdisplay/web-interface/src/components/CanvasArea.vue
2026-01-27 10:34:08 +01:00

280 lines
9.8 KiB
Vue

<template>
<main class="p-5 overflow-auto flex flex-col items-center gap-4 min-h-0">
<div class="w-full">
<div class="bg-gradient-to-b from-white to-[#f6f9fc] border border-border rounded-[14px] shadow-[0_10px_24px_rgba(15,23,42,0.12)] w-full pt-3 px-3.5" :class="screensOpen ? 'pb-3' : 'pb-2'">
<div class="flex flex-wrap items-center justify-between gap-2.5" :class="screensOpen ? 'mb-3' : 'mb-0'">
<div class="flex items-center gap-2">
<h3 class="text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]">Bildschirme</h3>
<span class="text-[11px] text-muted">{{ store.config.screens.length }}</span>
</div>
<div class="flex items-center gap-2">
<button class="border border-border bg-panel-2 text-text px-2.5 py-1.5 rounded-[10px] text-[12px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#e4ebf2] active:translate-y-0.5" @click="emit('open-screen-settings')">Einstellungen</button>
<button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="store.addScreen">+</button>
<button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="screensOpen = !screensOpen">
<span class="material-symbols-outlined text-[18px]">{{ screensOpen ? 'expand_less' : 'expand_more' }}</span>
</button>
</div>
</div>
<div v-if="screensOpen" class="flex flex-wrap gap-2">
<button
v-for="screen in store.config.screens"
:key="screen.id"
class="border rounded-full px-3 py-1.5 text-left text-[12px] inline-flex items-center gap-2 cursor-pointer transition hover:-translate-y-0.5"
:class="screen.id === store.activeScreenId ? 'bg-[#2f6db8] text-white border-[#2b62a5] shadow-[0_0_0_2px_rgba(47,109,184,0.2)]' : 'bg-[#eef3f7] text-[#2d3c4a] border-[#c8d2dc] hover:bg-white'"
@click="selectScreen(screen.id)"
>
<span class="max-w-[160px] overflow-hidden text-ellipsis whitespace-nowrap">{{ screen.name }}</span>
<span
class="text-[10px] px-1.5 py-0.5 rounded-full uppercase tracking-[0.06em]"
:class="screen.id === store.activeScreenId ? 'bg-white/20 text-white' : 'bg-[#dfe9f3] text-[#3a5f88]'"
>
{{ screen.mode === 1 ? 'Modal' : 'Fullscreen' }}
</span>
</button>
</div>
</div>
</div>
<div class="p-[18px] rounded-[18px] border border-border bg-white shadow-[0_10px_24px_rgba(15,23,42,0.12)]">
<div
class="relative border border-border overflow-hidden"
id="canvas"
:style="canvasStyle"
@click.self="deselect"
>
<div
class="pointer-events-none absolute inset-0 z-0 transition-opacity"
:class="store.showGrid ? 'opacity-50' : 'opacity-0'"
:style="gridStyle"
></div>
<WidgetElement
v-for="widget in rootWidgets"
:key="widget.id"
:widget="widget"
:scale="store.canvasScale"
:selected="store.selectedWidgetId === widget.id"
@select="store.selectedWidgetId = widget.id"
@drag-start="startDrag($event)"
@resize-start="startResize($event)"
/>
</div>
</div>
</main>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useEditorStore } from '../stores/editor';
import WidgetElement from './WidgetElement.vue';
import { DISPLAY_W, DISPLAY_H, WIDGET_TYPES } from '../constants';
import { clamp, minSizeFor } from '../utils';
const emit = defineEmits(['open-screen-settings']);
const store = useEditorStore();
const screensOpen = ref(true);
const rootWidgets = computed(() => {
return store.activeScreen?.widgets.filter(w => w.parentId === -1) || [];
});
const canvasW = computed(() => {
if (store.activeScreen?.mode === 1) { // Modal
return store.activeScreen.modal.w || DISPLAY_W;
}
return DISPLAY_W;
});
const canvasH = computed(() => {
if (store.activeScreen?.mode === 1) { // Modal
return store.activeScreen.modal.h || DISPLAY_H;
}
return DISPLAY_H;
});
const canvasStyle = computed(() => ({
width: `${canvasW.value * store.canvasScale}px`,
height: `${canvasH.value * store.canvasScale}px`,
backgroundColor: store.activeScreen?.bgColor || '#1A1A2E'
}));
const gridStyle = computed(() => ({
backgroundImage: 'linear-gradient(to right, rgba(255, 255, 255, 0.1) 1px, transparent 1px), linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px)',
backgroundSize: `${store.gridSize * store.canvasScale}px ${store.gridSize * store.canvasScale}px`
}));
function deselect() {
store.selectedWidgetId = null;
}
function selectScreen(id) {
store.activeScreenId = id;
store.selectedWidgetId = null;
}
function snap(val) {
if (!store.snapToGrid) return val;
const s = store.gridSize;
return Math.round(val / s) * s;
}
let dragState = null;
let resizeState = null;
function startDrag(payload) {
const { id, event: e } = payload;
if (e.target.closest('[data-resize-handle]')) return;
e.preventDefault();
store.selectedWidgetId = id;
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
const widget = store.activeScreen.widgets.find(w => w.id === id);
if (!widget) return;
dragState = {
id,
startX: cx,
startY: cy,
origX: widget.x,
origY: widget.y
};
resizeState = null;
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', endDrag);
}
function drag(e) {
if (!dragState) return;
e.preventDefault();
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
const dx = (cx - dragState.startX) / store.canvasScale;
const dy = (cy - dragState.startY) / store.canvasScale;
const w = store.activeScreen.widgets.find(x => x.id === dragState.id);
if (w) {
let nx = dragState.origX + dx;
let ny = dragState.origY + dy;
if (store.snapToGrid) {
nx = snap(nx);
ny = snap(ny);
}
// Determine bounds
let maxW = canvasW.value;
let maxH = canvasH.value;
if (w.parentId !== -1) {
const parent = store.activeScreen.widgets.find(p => p.id === w.parentId);
if (parent) {
if (parent.type === WIDGET_TYPES.TABPAGE) {
// Parent is a TabPage (w=0, h=0), so look at grandparent (TabView)
const grandParent = store.activeScreen.widgets.find(gp => gp.id === parent.parentId);
if (grandParent) {
maxW = grandParent.w;
maxH = grandParent.h;
// Adjust for tab bar? For simplicity use full size for now
}
} else {
maxW = parent.w;
maxH = parent.h;
}
}
}
w.x = Math.max(0, Math.min(maxW - w.w, Math.round(nx)));
w.y = Math.max(0, Math.min(maxH - w.h, Math.round(ny)));
}
}
function endDrag() {
dragState = null;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', endDrag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('touchend', endDrag);
}
function startResize(payload) {
const { id, event: e } = payload;
e.preventDefault();
store.selectedWidgetId = id;
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
const widget = store.activeScreen.widgets.find(w => w.id === id);
if (!widget) return;
resizeState = {
id,
startX: cx,
startY: cy,
origW: widget.w,
origH: widget.h
};
dragState = null;
document.addEventListener('mousemove', resizeDrag);
document.addEventListener('mouseup', endResize);
document.addEventListener('touchmove', resizeDrag, { passive: false });
document.addEventListener('touchend', endResize);
}
function resizeDrag(e) {
if (!resizeState) return;
e.preventDefault();
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
const dx = (cx - resizeState.startX) / store.canvasScale;
const dy = (cy - resizeState.startY) / store.canvasScale;
const w = store.activeScreen.widgets.find(x => x.id === resizeState.id);
if (!w) return;
const minSize = minSizeFor(w);
const maxW = canvasW.value - w.x;
const maxH = canvasH.value - w.y;
let rawW = resizeState.origW + dx;
let rawH = resizeState.origH + dy;
if (store.snapToGrid) {
rawW = snap(rawW);
rawH = snap(rawH);
}
let newW = Math.round(rawW);
let newH = Math.round(rawH);
if (w.type === WIDGET_TYPES.LED) {
const maxSize = Math.min(maxW, maxH);
const size = clamp(Math.max(newW, newH), minSize.w, maxSize);
newW = size;
newH = size;
} else {
newW = clamp(newW, minSize.w, maxW);
newH = clamp(newH, minSize.h, maxH);
}
w.w = newW;
w.h = newH;
}
function endResize() {
resizeState = null;
document.removeEventListener('mousemove', resizeDrag);
document.removeEventListener('mouseup', endResize);
document.removeEventListener('touchmove', resizeDrag);
document.removeEventListener('touchend', endResize);
}
</script>