neko/src/component/overlay.vue

337 lines
9.0 KiB
Vue
Raw Normal View History

2020-11-07 05:59:01 +13:00
<template>
<canvas
2020-11-07 05:59:01 +13:00
ref="overlay"
2021-02-11 08:57:13 +13:00
class="neko-overlay"
:class="isControling ? 'neko-active' : ''"
2020-11-07 05:59:01 +13:00
tabindex="0"
2021-01-10 11:28:56 +13:00
:style="{ cursor }"
2020-11-07 05:59:01 +13:00
@click.stop.prevent
@contextmenu.stop.prevent
@wheel.stop.prevent="onWheel"
@mousemove.stop.prevent="onMouseMove"
@mousedown.stop.prevent="onMouseDown"
@mouseup.stop.prevent="onMouseUp"
@mouseenter.stop.prevent="onMouseEnter"
@mouseleave.stop.prevent="onMouseLeave"
@dragenter.stop.prevent="onDragEnter"
@dragleave.stop.prevent="onDragLeave"
@dragover.stop.prevent="onDragOver"
2021-01-08 08:21:40 +13:00
@drop.stop.prevent="onDrop"
/>
2020-11-07 05:59:01 +13:00
</template>
<style lang="scss" scoped>
2021-02-11 08:57:13 +13:00
.neko-overlay {
2020-11-07 05:59:01 +13:00
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
2020-11-07 08:48:19 +13:00
outline: 0;
2021-02-11 08:54:44 +13:00
2021-02-11 08:57:13 +13:00
&.neko-active {
2021-02-11 08:54:44 +13:00
outline: 2px solid red;
box-sizing: border-box;
}
2021-02-12 07:03:31 +13:00
.cursor {
position: relative;
}
2020-11-07 05:59:01 +13:00
}
</style>
<script lang="ts">
2020-12-02 22:45:23 +13:00
import { Vue, Component, Ref, Prop, Watch } from 'vue-property-decorator'
2020-11-07 08:48:19 +13:00
2020-11-29 09:47:16 +13:00
import GuacamoleKeyboard from './utils/guacamole-keyboard'
import { getFilesFromDataTansfer } from './utils/file-upload'
2020-11-29 09:47:16 +13:00
import { NekoWebRTC } from './internal/webrtc'
2021-01-10 11:28:56 +13:00
import { Control } from './types/state'
2020-11-07 05:59:01 +13:00
2021-02-11 08:49:02 +13:00
const inactiveCursorWin10 =
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACEUlEQVR4nOzWz6sSURQH8O+89zJ5C32LKbAgktCSaPpBSL' +
'uSNtHqLcOV+BeIGxei0oCtFME/wI0bF4GCK6mNuAghH7xFlBAO7bQoA/Vik3riyghTaCQzTsLzbIZZDPdzzj3nzt3Df44dYDsBRNSYTqcn5XL5KoADy1VERL' +
'Is02g0+phIJG4BsFkOEEVxjhgOh59kWb5rKWIBWCAGg0EnFovdtgyhB+grkU6n7wA4ZzlgCWKzlVgGsLQnVgE2gVh7xvP5PH9ciUajFQDHyWTyHQDVKOS3+F' +
'sF/pyOcDh83Uhj/nMFBEFANpuF0+nUQ92SJD0G8AXAdwAz0wE+nw8OhwPNZhPj8RiBQOC0Vqu9EgSBcrnc11Qq9R7AeW5cd/GVsdgCr9dLiqJQtVqdv/v9fm' +
'KM9UVRfArgJoBrAC4DsJsOcLlc1Gg0qNVqVRljI0mS5oh6vU6lUukFgEta5gemLr4AFAoF6nQ6b20223G73X6ZyWTmgFAoRL1ej3f+DQ1gfqiq+qbf73/weD' +
'zPADwoFouPut3uzO12UyQSoclkotrt9ocAHKZnr8UhAP4bvg/gIs+UMfaaMTZTFOUkHo8/B/AEwAWjl5pV+j1dZ//g4xUMBo8YY/cqlcqhNvffAJxq40dmA5' +
'bFPoAjrev5EfwZQNfoKbju/u1ri/PvfgKYGMl+K2I7b8U7wA5wpgC/AgAA///Yyif1MZXzRQAAAABJRU5ErkJggg==) 4 4, crosshair'
2020-11-07 05:59:01 +13:00
@Component({
name: 'neko-overlay',
})
export default class extends Vue {
@Ref('overlay') readonly _overlay!: HTMLCanvasElement
private _ctx: any = null
2020-11-07 05:59:01 +13:00
private keyboard = GuacamoleKeyboard()
private focused = false
@Prop()
private readonly webrtc!: NekoWebRTC
@Prop()
2021-01-10 11:28:56 +13:00
private readonly control!: Control
2020-11-07 05:59:01 +13:00
@Prop()
2021-02-13 05:11:28 +13:00
private readonly screenSize: { width: number; height: number } = {
width: 0,
height: 0,
}
2020-11-07 05:59:01 +13:00
@Prop()
2021-02-13 05:11:28 +13:00
private readonly canvasSize: { width: number; height: number } = {
width: 0,
height: 0,
}
2020-11-07 05:59:01 +13:00
@Prop()
private readonly isControling!: boolean
2020-12-02 22:45:23 +13:00
@Prop()
private readonly implicitControl!: boolean
2021-01-10 11:28:56 +13:00
get cursor(): string {
2021-02-11 08:49:02 +13:00
if (!this.isControling) {
return inactiveCursorWin10
}
2021-02-12 07:03:31 +13:00
if (!this.control.cursor.image) {
2021-02-11 08:49:02 +13:00
return 'auto'
}
2021-01-10 11:28:56 +13:00
2021-02-12 07:03:31 +13:00
const { uri, x, y } = this.control.cursor.image
2021-01-11 00:34:08 +13:00
return 'url(' + uri + ') ' + x + ' ' + y + ', auto'
2021-01-10 11:28:56 +13:00
}
2020-11-07 05:59:01 +13:00
mounted() {
this._ctx = this._overlay.getContext('2d')
// synchronize intrinsic with extrinsic dimensions
const { width, height } = this._overlay.getBoundingClientRect()
this._overlay.width = width
this._overlay.height = height
2020-11-07 05:59:01 +13:00
// Initialize Guacamole Keyboard
this.keyboard.onkeydown = (key: number) => {
2020-12-02 22:45:23 +13:00
if (!this.focused) {
return true
}
if (!this.isControling) {
2020-11-07 05:59:01 +13:00
return true
}
this.webrtc.send('keydown', { key })
return false
}
this.keyboard.onkeyup = (key: number) => {
2020-12-02 22:45:23 +13:00
if (!this.focused) {
return
}
if (!this.isControling) {
2020-11-07 05:59:01 +13:00
return
}
this.webrtc.send('keyup', { key })
}
this.keyboard.listenTo(this._overlay)
}
beforeDestroy() {
// Guacamole Keyboard does not provide destroy functions
}
2021-01-08 08:21:40 +13:00
getMousePos(clientX: number, clientY: number) {
2020-11-07 05:59:01 +13:00
const rect = this._overlay.getBoundingClientRect()
2021-01-08 08:21:40 +13:00
return {
2021-02-13 05:11:28 +13:00
x: Math.round((this.screenSize.width / rect.width) * (clientX - rect.left)),
y: Math.round((this.screenSize.height / rect.height) * (clientY - rect.top)),
2021-01-08 08:21:40 +13:00
}
}
private mousepos: { x: number; y: number } = { x: 0, y: 0 }
2021-01-08 08:21:40 +13:00
setMousePos(e: MouseEvent) {
2021-02-12 07:03:31 +13:00
const pos = this.getMousePos(e.clientX, e.clientY)
this.webrtc.send('mousemove', pos)
Vue.set(this, 'mousepos', pos)
2020-11-07 05:59:01 +13:00
}
onWheel(e: WheelEvent) {
if (!this.isControling) {
return
}
let x = e.deltaX
let y = e.deltaY
2021-01-10 11:28:56 +13:00
if (this.control.scroll.inverse) {
2020-11-07 05:59:01 +13:00
x *= -1
y *= -1
}
2021-01-10 11:28:56 +13:00
x = Math.min(Math.max(x, -this.control.scroll.sensitivity), this.control.scroll.sensitivity)
y = Math.min(Math.max(y, -this.control.scroll.sensitivity), this.control.scroll.sensitivity)
2020-11-07 05:59:01 +13:00
this.setMousePos(e)
this.webrtc.send('wheel', { x, y })
}
onMouseMove(e: MouseEvent) {
if (!this.isControling) {
return
}
this.setMousePos(e)
}
onMouseDown(e: MouseEvent) {
if (!this.isControling) {
2021-02-11 08:49:02 +13:00
this.implicitControlRequest(e)
2020-11-07 05:59:01 +13:00
return
}
this.setMousePos(e)
this.webrtc.send('mousedown', { key: e.button + 1 })
}
onMouseUp(e: MouseEvent) {
if (!this.isControling) {
2021-02-11 08:49:02 +13:00
this.implicitControlRequest(e)
2020-11-07 05:59:01 +13:00
return
}
this.setMousePos(e)
this.webrtc.send('mouseup', { key: e.button + 1 })
}
onMouseEnter(e: MouseEvent) {
this._overlay.focus()
this.focused = true
if (!this.isControling) {
2020-12-02 22:45:23 +13:00
// TODO: Refactor
//syncKeyboardModifierState({
// capsLock: e.getModifierState('CapsLock'),
// numLock: e.getModifierState('NumLock'),
// scrollLock: e.getModifierState('ScrollLock'),
//})
2020-11-07 05:59:01 +13:00
}
2020-12-02 22:45:23 +13:00
}
2020-11-07 05:59:01 +13:00
2020-12-02 22:45:23 +13:00
onMouseLeave(e: MouseEvent) {
2020-11-07 05:59:01 +13:00
this._overlay.blur()
this.focused = false
2020-12-02 22:45:23 +13:00
if (this.isControling) {
this.keyboard.reset()
// TODO: Refactor
//setKeyboardModifierState({
// capsLock: e.getModifierState('CapsLock'),
// numLock: e.getModifierState('NumLock'),
// scrollLock: e.getModifierState('ScrollLock'),
//})
}
}
onDragEnter(e: DragEvent) {
this.onMouseEnter(e as MouseEvent)
2021-01-08 08:21:40 +13:00
}
onDragLeave(e: DragEvent) {
this.onMouseLeave(e as MouseEvent)
}
onDragOver(e: DragEvent) {
this.onMouseMove(e as MouseEvent)
}
2021-01-08 08:21:40 +13:00
async onDrop(e: DragEvent) {
if (this.isControling || this.implicitControl) {
let dt = e.dataTransfer
if (!dt) return
2021-01-08 08:21:40 +13:00
const files = await getFilesFromDataTansfer(dt)
if (files.length === 0) return
2021-01-08 08:21:40 +13:00
this.$emit('drop-files', { ...this.getMousePos(e.clientX, e.clientY), files })
}
2021-01-08 08:21:40 +13:00
}
2021-02-13 05:11:28 +13:00
@Watch('canvasSize')
onCanvasSizeChange({ width, height }: { width: number; height: number }) {
this._overlay.width = width
this._overlay.height = height
}
private cursorElem: HTMLImageElement = new Image()
@Watch('control.cursor.image')
onCursorImageChange({ uri }: { uri: string }) {
this.cursorElem.src = uri
}
@Watch('control.cursor.position')
onCursorPositionChange({ x, y }: { x: number; y: number }) {
if (this.isControling || this.control.cursor.image == null) return
2021-02-13 05:11:28 +13:00
// get intrinsic dimensions
const { width, height } = this._overlay
// redraw cursor
this._ctx.clearRect(0, 0, width, height)
this._ctx.drawImage(
this.cursorElem,
2021-02-13 05:11:28 +13:00
(x / this.screenSize.width) * width - this.control.cursor.image.x,
(y / this.screenSize.height) * height - this.control.cursor.image.y,
this.control.cursor.image.width,
this.control.cursor.image.height,
)
}
2021-02-11 08:49:02 +13:00
private reqMouseDown: any | null = null
private reqMouseUp: any | null = null
2020-12-02 22:45:23 +13:00
@Watch('isControling')
onControlChange(isControling: boolean) {
2021-02-11 08:49:02 +13:00
if (isControling && this.reqMouseDown) {
this.setMousePos(this.reqMouseDown)
this.webrtc.send('mousedown', { key: this.reqMouseDown.button + 1 })
}
if (isControling && this.reqMouseUp) {
this.webrtc.send('mouseup', { key: this.reqMouseUp.button + 1 })
}
if (isControling) {
const { width, height } = this._overlay
this._ctx.clearRect(0, 0, width, height)
} else {
this.onCursorPositionChange(this.mousepos)
}
2021-02-11 08:49:02 +13:00
this.reqMouseDown = null
this.reqMouseUp = null
2020-12-02 22:45:23 +13:00
}
2021-02-11 08:49:02 +13:00
implicitControlRequest(e: MouseEvent) {
if (this.implicitControl && e.type === 'mousedown' && this.reqMouseDown == null) {
this.reqMouseDown = e
2020-12-02 22:45:23 +13:00
this.$emit('implicit-control-request')
}
2021-02-11 08:49:02 +13:00
if (this.implicitControl && e.type === 'mouseup' && this.reqMouseUp == null) {
this.reqMouseUp = e
}
2020-12-02 22:45:23 +13:00
}
2021-02-11 08:49:02 +13:00
// unused
2020-12-02 22:45:23 +13:00
implicitControlRelease() {
if (this.implicitControl) {
this.$emit('implicit-control-release')
}
2020-11-07 05:59:01 +13:00
}
}
</script>