feat(stage-tamagotchi): added electron displays devtools

This commit is contained in:
Neko Ayaka
2025-10-18 23:39:49 +08:00
parent 2370a31521
commit e25d5fa566
2 changed files with 167 additions and 0 deletions
@@ -0,0 +1,161 @@
<script setup lang="ts">
import { defineInvoke } from '@unbird/eventa'
import { createContext } from '@unbird/eventa/adapters/electron/renderer'
import { computedAsync, useIntervalFn, useWindowSize } from '@vueuse/core'
import { computed, ref } from 'vue'
import { electron } from '../../../shared/electron'
const { context: ctx } = createContext(window.electron.ipcRenderer)
const getAllDisplays = defineInvoke(ctx, electron.screen.getAllDisplays)
const getCursorScreenPoint = defineInvoke(ctx, electron.screen.getCursorScreenPoint)
const allDisplays = computedAsync(async () => getAllDisplays(), [])
const cursorPoint = ref({ x: 0, y: 0 })
// Update cursor position periodically
useIntervalFn(async () => {
const newPoint = await getCursorScreenPoint()
cursorPoint.value = newPoint
}, 1000 / 60)
// Get container element and its bounds
const containerRef = ref<HTMLElement>()
const windowSize = useWindowSize()
// Calculate bounds for all displays
const displayBounds = computed(() => {
if (!allDisplays.value.length)
return { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 }
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (const display of allDisplays.value) {
minX = Math.min(minX, display.bounds.x)
minY = Math.min(minY, display.bounds.y)
maxX = Math.max(maxX, display.bounds.x + display.bounds.width)
maxY = Math.max(maxY, display.bounds.y + display.bounds.height)
}
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
}
})
// Responsive scale factor based on available window/container space
const scale = computed(() => {
const { width: totalWidth, height: totalHeight } = displayBounds.value
if (totalWidth === 0 || totalHeight === 0)
return 0.2
// Use window size with some padding for responsive scaling
const padding = 100
const availableWidth = windowSize.width.value - padding
const availableHeight = windowSize.height.value - padding
// Calculate scale to fit within available space
const scaleX = availableWidth / totalWidth
const scaleY = availableHeight / totalHeight
// Use the smaller scale to ensure everything fits, with a max scale of 0.3
return Math.min(scaleX, scaleY, 0.3)
})
// Transform display coordinates
function transformDisplay(display: any) {
const { minX, minY } = displayBounds.value
return {
x: (display.bounds.x - minX) * scale.value,
y: (display.bounds.y - minY) * scale.value,
width: display.bounds.width * scale.value,
height: display.bounds.height * scale.value,
}
}
// Transform cursor coordinates
const transformedCursor = computed(() => {
const { minX, minY } = displayBounds.value
return {
x: (cursorPoint.value.x - minX) * scale.value,
y: (cursorPoint.value.y - minY) * scale.value,
}
})
// Calculate container dimensions
const containerDimensions = computed(() => {
const { width, height } = displayBounds.value
return {
width: width * scale.value,
height: height * scale.value,
}
})
</script>
<template>
<div>
<div
ref="containerRef"
class="relative"
:style="{
width: `${containerDimensions.width}px`,
height: `${containerDimensions.height}px`,
}"
>
<div
v-for="display in allDisplays"
:key="display.id"
class="absolute box-border border-2 border-neutral-200 bg-neutral-50/50 dark:border-neutral-800 dark:bg-neutral-950/50"
:style="{
left: `${transformDisplay(display).x}px`,
top: `${transformDisplay(display).y}px`,
width: `${transformDisplay(display).width}px`,
height: `${transformDisplay(display).height}px`,
}"
>
<!-- Work area (slightly darker) -->
<div
class="absolute bg-neutral-200/50 dark:bg-neutral-800/50"
:style="{
left: `${(display.workArea.x - display.bounds.x) * scale}px`,
top: `${(display.workArea.y - display.bounds.y) * scale}px`,
width: `${display.workArea.width * scale}px`,
height: `${display.workArea.height * scale}px`,
}"
/>
<div class="pointer-events-none absolute left-2.5 top-2.5 text-xs text-neutral-800 font-medium">
{{ display.label }}
</div>
<div class="pointer-events-none absolute left-2.5 top-7.5 text-[10px] text-neutral-600">
{{ display.bounds.width }}x{{ display.bounds.height }} @ {{ display.rotation }}°
</div>
</div>
<div
class="pointer-events-none absolute h-3 w-3 border-2 border-primary-400 rounded-full bg-primary-400 transition-all duration-150 ease-out -ml-1.5 -mt-1.5"
:style="{
left: `${transformedCursor.x}px`,
top: `${transformedCursor.y}px`,
}"
>
<div class="absolute left-3 whitespace-nowrap text-[11px] text-primary-400 font-bold -top-1.25">
{{ cursorPoint.x }}, {{ cursorPoint.y }}
</div>
</div>
</div>
</div>
</template>
<route lang="yaml">
meta:
layout: settings
</route>
@@ -24,6 +24,12 @@ const menu = computed(() => [
icon: 'i-solar:sledgehammer-bold-duotone',
to: '/devtools/use-window-mouse',
},
{
title: 'Displays',
description: 'Visualize connected displays and cursor position',
icon: 'i-solar:sledgehammer-bold-duotone',
to: '/devtools/electron-display',
},
])
const { context } = createContext(window.electron.ipcRenderer)