Skip to content
Open
2 changes: 1 addition & 1 deletion images/chromium-headful/client/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<title>Kernel</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMTEiIGhlaWdodD0iMTM5IiB2aWV3Qm94PSIwIDAgMTExIDEzOSIgZmlsbD0ibm9uZSI+CjxwYXRoIGQ9Ik0xMS4xNzUzIDBDMTQuMjY3MiAwIDE2Ljg5NDIgMS4wOTc2IDE5LjA1NDggMy4yOTMxMUMyMS4yNTI2IDUuNDUxNCAyMi4zNTEzIDguMDc1MTYgMjIuMzUxMyAxMS4xNjM3VjE2LjU1NjJDMjIuMzUxMyAxNy4yMjYxIDIzLjIwMzUgMTcuNTExMSAyMy42MDY0IDE2Ljk3NTlMMzMuMDI0NCA0LjQ2NTc2QzM0Ljg0OTggMi4wMDk3NSAzNy4yODk4IDAuNTc2OTU2IDQwLjM0NDUgMC4xNjc2MTlDNDMuMzk5IC0wLjI3ODgzIDQ2LjE3NDUgMC40MDk1NSA0OC42NzAzIDIuMjMyODhDNTEuMTI4NyA0LjA5MzQ0IDUyLjU2MjcgNi41NDk0MiA1Mi45NzI1IDkuNjAwNjRDNTMuNDE5NSAxMi42NTIxIDUyLjcxMTkgMTUuNDA2NCA1MC44NDkzIDE3Ljg2MjRMNDEuMTM5IDMwLjgxMjdDNDAuNzY3MyAzMS4zMDg0IDQwLjc2NyAzMS45OTAxIDQxLjEzODMgMzIuNDg2MUw1My41ODcxIDQ5LjEyMDdDNTUuNDQ5NyA1MS41NzY3IDU2LjE1OCA1NC4zMzAzIDU1LjcxMSA1Ny4zODE3QzU1LjMwMTIgNjAuNDMzMSA1My44NjY2IDYyLjg4ODkgNTEuNDA4MSA2NC43NDk1QzQ4LjkxMjIgNjYuNTcyOSA0Ni4xMzY5IDY3LjI2MTMgNDMuMDgyMyA2Ni44MTQ4QzQwLjAyNzcgNjYuNDA1NCAzNy41ODc2IDY0Ljk3MzEgMzUuNzYyMiA2Mi41MTczTDIzLjYwNTcgNDYuNDI3MkMyMy4yMDIzIDQ1Ljg5MzIgMjIuMzUxMyA0Ni4xNzgzIDIyLjM1MTMgNDYuODQ3NlY1NS44MTg2QzIyLjM1MTMgNTguOTA3MiAyMS4yNTI2IDYxLjU0OTYgMTkuMDU0OCA2My43NDUxQzE2Ljg5NDIgNjUuOTAzNCAxNC4yNjcyIDY2Ljk4MjQgMTEuMTc1MyA2Ni45ODI0QzguMDgzNTMgNjYuOTgyMyA1LjQzODQ0IDY1LjkwMzQgMy4yNDA2NCA2My43NDUxQzEuMDgwMDYgNjEuNTQ5NiAwIDU4LjkwNzIgMCA1NS44MTg2VjExLjE2MzdDMS4wOTI0OGUtMDUgOC4wNzUxNCAxLjA4MDA2IDUuNDUxNDEgMy4yNDA2NCAzLjI5MzExQzUuNDM4NDQgMS4wOTc2NiA4LjA4MzUzIDguMzA0OTFlLTA1IDExLjE3NTMgMFoiIGZpbGw9IiM4MUIzMDAiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik04Ni41OTE4IDAuNTU4MDVDOTIuNzc1NiAwLjU1ODA1NiA5OC4wNDY4IDIuNzM1MzEgMTAyLjQwNSA3LjA4OTA4QzEwNi43NjQgMTEuNDQyOSAxMDguOTQzIDE2LjcwODMgMTA4Ljk0MyAyMi44ODU1QzEwOC45NDMgMjkuMDYyOCAxMDYuNzY0IDM0LjMyODggMTAyLjQwNSAzOC42ODI2QzEwMi4yNDggMzguODQwMSAxMDIuMDkgMzguOTkzNiAxMDEuOTMyIDM5LjE0MzJDMTAxLjM5NiAzOS42NTE4IDEwMS4yOTkgNDAuNDgzNiAxMDEuNzQyIDQxLjA3NDlMMTA3Ljc3IDQ5LjEyMDdDMTA5LjYzMiA1MS41NzY3IDExMC4zNCA1NC4zMzA0IDEwOS44OTMgNTcuMzgxN0MxMDkuNDgzIDYwLjQzMyAxMDguMDQ5IDYyLjg4ODkgMTA1LjU5MSA2NC43NDk1QzEwMy4wOTUgNjYuNTcyOCAxMDAuMzIgNjcuMjYxMiA5Ny4yNjQ5IDY2LjgxNDhDOTQuMjEwNCA2Ni40MDU0IDkxLjc3MDIgNjQuOTczMSA4OS45NDQ5IDYyLjUxNzNMODIuMzA2NiA0OS4yMTI3QzgxLjk1MDYgNDguNTkzMiA4MS4wMDM5IDQ4Ljg0NTYgODEuMDAzOCA0OS41NjAyVjU1LjgxODZDODEuMDAzOCA1OC45MDczIDc5LjkwNTIgNjEuNTQ5NiA3Ny43MDczIDYzLjc0NTFDNzUuNTQ2NyA2NS45MDM0IDcyLjkyMDQgNjYuOTgyMyA2OS44Mjg1IDY2Ljk4MjRDNjYuNzM2NiA2Ni45ODI0IDY0LjA5MTcgNjUuOTAzMyA2MS44OTM4IDYzLjc0NTFDNTkuNzMzMiA2MS41NDk2IDU4LjY1MjUgNTguOTA3MyA1OC42NTI1IDU1LjgxODZWMTEuNzIxOEM1OC42NTI1IDguNjMzMjEgNTkuNzMzMiA2LjAwOTQ1IDYxLjg5MzggMy44NTExNkM2NC4wOTE3IDEuNjU1NzggNjYuNzM2NyAwLjU1ODA1MyA2OS44Mjg1IDAuNTU4MDVIODYuNTkxOFpNODMuNTI4MyAxOC4xNDExQzgxLjAyMzYgMTguMTQxMSA3OC45OTMgMjAuMTcxNiA3OC45OTMgMjIuNjc2M0M3OC45OTMgMjUuMTgxMSA4MS4wMjM2IDI3LjIxMTYgODMuNTI4MyAyNy4yMTE2Qzg2LjAzMzEgMjcuMjExNiA4OC4wNjM2IDI1LjE4MTEgODguMDYzNiAyMi42NzYzQzg4LjA2MzYgMjAuMTcxNiA4Ni4wMzMxIDE4LjE0MTEgODMuNTI4MyAxOC4xNDExWiIgZmlsbD0iIzgxQjMwMCIvPgo8cGF0aCBkPSJNNTAuMjkwNiA3MS41MTc2QzUzLjM4MjMgNzEuNTE3NyA1Ni4wMDgyIDcyLjYxNTUgNTguMTY4NyA3NC44MTA3QzYwLjM2NjYgNzYuOTY5MSA2MS40NjU5IDc5LjU5MjcgNjEuNDY1OSA4Mi42ODE0VjEyNy4zMzZDNjEuNDY1OSAxMzAuNDI1IDYwLjM2NjYgMTMzLjA2NyA1OC4xNjg3IDEzNS4yNjNDNTYuMDA4MiAxMzcuNDIxIDUzLjM4MjMgMTM4LjUgNTAuMjkwNiAxMzguNUM0Ny4xOTg4IDEzOC41IDQ0LjU3MjQgMTM3LjQwMiA0Mi40MTE4IDEzNS4yMDdMMjMuNTczIDExMy43NjdDMjMuMTQ4MyAxMTMuMjg0IDIyLjM1MTUgMTEzLjU4NSAyMi4zNTEzIDExNC4yMjhWMTI3LjMzNkMyMi4zNTEzIDEzMC40MjUgMjEuMjUyIDEzMy4wNjcgMTkuMDU0MSAxMzUuMjYzQzE2Ljg5MzUgMTM3LjQyMSAxNC4yNjcyIDEzOC41IDExLjE3NTMgMTM4LjVDOC4wODM0NyAxMzguNSA1LjQzODQ4IDEzNy40MjEgMy4yNDA2NCAxMzUuMjYzQzEuMDgwMDQgMTMzLjA2NyAwIDEzMC40MjUgMCAxMjcuMzM2VjgyLjY4MTRDMS4wOTI0OWUtMDUgNzkuNTkyNyAxLjA4MDAzIDc2Ljk2OTEgMy4yNDA2NCA3NC44MTA3QzUuNDM4NDggNzIuNjE1MyA4LjA4MzQ2IDcxLjUxNzcgMTEuMTc1MyA3MS41MTc2QzE0LjI2NzEgNzEuNTE3NiAxNi45MTIxIDcyLjU5NjcgMTkuMTEgNzQuNzU0OUwzNy44OTIyIDk2LjE4OTdDMzguMzE2NyA5Ni42NzQgMzkuMTE0NiA5Ni4zNzM4IDM5LjExNDYgOTUuNzI5OFY4Mi42ODE0QzM5LjExNDYgNzkuNTkyNyA0MC4xOTUzIDc2Ljk2OTEgNDIuMzU1OSA3NC44MTA3QzQ0LjU1MzggNzIuNjE1NCA0Ny4xOTg4IDcxLjUxNzYgNTAuMjkwNiA3MS41MTc2WiIgZmlsbD0iIzgxQjMwMCIvPgo8cGF0aCBkPSJNNzYuNzMyOSA3MS41MTc2Qzc5LjgyNDcgNzEuNTE3NiA4Mi40NTExIDcyLjYxNTQgODQuNjExNyA3NC44MTA3Qzg2LjgwOTYgNzYuOTY5MSA4Ny45MDg5IDc5LjU5MjcgODcuOTA4OSA4Mi42ODE0VjExNC4yMTlDODcuOTA5IDExNC45OSA4OC41MzM3IDExNS42MTQgODkuMzA0NCAxMTUuNjE0SDk5LjA4NDlDMTAyLjE3NyAxMTUuNjE1IDEwNC44MDMgMTE2LjcxMiAxMDYuOTY0IDExOC45MDhDMTA5LjE2MSAxMjEuMDY2IDExMC4yNiAxMjMuNjkgMTEwLjI2IDEyNi43NzhDMTEwLjI2IDEyOS44NjcgMTA5LjE2MiAxMzIuNTA5IDEwNi45NjQgMTM0LjcwNUMxMDQuODAzIDEzNi44NjMgMTAyLjE3NyAxMzcuOTQyIDk5LjA4NDkgMTM3Ljk0Mkg3Ni43MzI5QzczLjY0MTMgMTM3Ljk0MiA3MC45OTY3IDEzNi44NjMgNjguNzk4OSAxMzQuNzA1QzY2LjYzODMgMTMyLjUwOSA2NS41NTc2IDEyOS44NjcgNjUuNTU3NiAxMjYuNzc4VjgyLjY4MTRDNjUuNTU3NiA3OS41OTI3IDY2LjYzODMgNzYuOTY5MSA2OC43OTg5IDc0LjgxMDdDNzAuOTk2NyA3Mi42MTU1IDczLjY0MTMgNzEuNTE3NyA3Ni43MzI5IDcxLjUxNzZaIiBmaWxsPSIjODFCMzAwIi8+CjxwYXRoIGQ9Ik0xMDEuNTkzIDcxLjUxNzZDMTA2LjM4IDcxLjUxNzYgMTEwLjI2IDc1LjM5MzggMTEwLjI2IDgwLjE3NTJDMTEwLjI2IDg0Ljk1NjggMTA2LjM4IDg4LjgzMjkgMTAxLjU5MyA4OC44MzI5Qzk2LjgwNjUgODguODMyOCA5Mi45MjY2IDg0Ljk1NjcgOTIuOTI2NiA4MC4xNzUyQzkyLjkyNjcgNzUuMzkzOCA5Ni44MDY1IDcxLjUxNzcgMTAxLjU5MyA3MS41MTc2WiIgZmlsbD0iIzgxQjMwMCIvPgo8L3N2Zz4K">
<link rel="manifest" href="site.webmanifest">
Expand Down
5 changes: 4 additions & 1 deletion images/chromium-headful/client/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@
bottom: 0;
max-width: 100vw;
max-height: 100vh;
max-height: 100dvh;
flex-direction: row;
display: flex;

.neko-main {
min-width: 360px;
min-width: 0;
max-width: 100%;
flex-grow: 1;
flex-direction: column;
Expand Down Expand Up @@ -144,10 +145,12 @@

.neko-main {
height: 100vh;
height: 100dvh;
}

.neko-menu {
height: 100vh;
height: 100dvh;
width: 100% !important;
}
}
Expand Down
228 changes: 228 additions & 0 deletions images/chromium-headful/client/src/components/touch-controls.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<template>
<div class="touch-controls">
<button
ref="control"
class="keyboard-control"
data-role="keyboard-toggle"
:class="[`mode-${mode}`, { dragging: !!dragPoint, faded, open: keyboardOpen }]"
:style="controlStyle"
:aria-label="keyboardOpen ? 'Hide keyboard' : 'Keyboard'"
@touchstart.stop.prevent="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchEnd"
@touchcancel.stop.prevent="onTouchCancel"
@click.stop.prevent="toggle"
>
<i class="fas fa-keyboard" />
<span v-if="mode === 'bottom'">{{ keyboardOpen ? 'Hide keyboard' : 'Keyboard' }}</span>
<span v-else-if="keyboardOpen" class="short">Hide</span>
</button>
</div>
</template>

<style lang="scss" scoped>
.touch-controls {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
pointer-events: none;
}

.keyboard-control {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 44px;
min-height: 44px;
padding: 0 12px;
border: 0;
border-radius: 22px;
background: rgba($color: #000, $alpha: 0.7);
color: #fff;
font-size: 15px;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
pointer-events: auto;
touch-action: none;
-webkit-tap-highlight-color: transparent;
transition: opacity 300ms ease, left 200ms ease-out, top 200ms ease-out;

&.dragging {
transition: opacity 300ms ease;
}

// in a side strip the button is only as wide as the strip
&.mode-left,
&.mode-right {
flex-direction: column;
gap: 2px;
padding: 6px 0;

.short {
font-size: 10px;
}
}

&.open {
background: #81b300;
color: #000;
}

&.faded {
opacity: 0.3;
}

// over the stream a faded control lets taps through; any touch on the view wakes it
&.mode-overlay.faded {
pointer-events: none;
}
}
</style>

<script lang="ts">
import { Component, Prop, Ref, Vue, Watch } from 'vue-property-decorator'
import {
ControlFrame,
ControlMode,
ControlPosition,
SafeArea,
Size,
dropControl,
placeControl,
} from '~/utils/touch-controls'

const IDLE_FADE_MS = 3000
const DRAG_SLOP = 6

interface Point {
x: number
y: number
}

@Component({ name: 'neko-touch-controls' })
export default class extends Vue {
@Ref('control') readonly _control!: HTMLElement

@Prop({ type: String, required: true }) readonly mode!: ControlMode
@Prop({ type: Object, required: true }) readonly position!: ControlPosition
@Prop({ type: Object, required: true }) readonly safeArea!: SafeArea
@Prop(Boolean) readonly keyboardOpen!: boolean
@Prop({ type: Number, default: 0 }) readonly areaWidth!: number
@Prop({ type: Number, default: 0 }) readonly areaHeight!: number
@Prop({ type: Number, default: 0 }) readonly keyboardInset!: number

private size: Size = { width: 44, height: 44 }
private faded = false
private fadeTimer = 0
private touchStart: Point | null = null
// finger offset from the control's top-left corner, so a drag does not jump
private grab: Point = { x: 0, y: 0 }
private dragPoint: Point | null = null

get frame(): ControlFrame {
return {
area: { width: this.areaWidth, height: this.areaHeight },
control: this.size,
safe: this.safeArea,
keyboardInset: this.keyboardInset,
}
}

get corner(): Point {
if (this.dragPoint) {
return { x: this.dragPoint.x - this.grab.x, y: this.dragPoint.y - this.grab.y }
}
return placeControl(this.mode, this.position, this.frame)
}

get controlStyle() {
return { left: `${this.corner.x}px`, top: `${this.corner.y}px` }
}

mounted() {
this.measure()
this.wake()
}

updated() {
this.measure()
}

beforeDestroy() {
window.clearTimeout(this.fadeTimer)
}

@Watch('keyboardOpen')
onKeyboardOpen() {
this.wake()
}

// the label changes the control's size, which its placement depends on
measure() {
const { offsetWidth: width, offsetHeight: height } = this._control
if (width !== this.size.width || height !== this.size.height) {
this.size = { width, height }
}
}

// restore full opacity and restart the idle timer
wake() {
this.faded = false
window.clearTimeout(this.fadeTimer)
this.fadeTimer = window.setTimeout(() => {
this.faded = !this.keyboardOpen && !this.dragPoint
}, IDLE_FADE_MS)
}

toggle() {
this.wake()
this.$emit('toggle')
}

localPoint(t: Touch): Point {
const rect = this.$el.getBoundingClientRect()
return { x: t.clientX - rect.left, y: t.clientY - rect.top }
}

onTouchStart(e: TouchEvent) {
this.wake()
const p = this.localPoint(e.changedTouches[0])
const corner = this.corner
this.touchStart = p
this.grab = { x: p.x - corner.x, y: p.y - corner.y }
}

onTouchMove(e: TouchEvent) {
if (!this.touchStart) return
const p = this.localPoint(e.changedTouches[0])
if (!this.dragPoint && Math.hypot(p.x - this.touchStart.x, p.y - this.touchStart.y) < DRAG_SLOP) return
this.dragPoint = p
}

// runs inside touchend, so a tap can still raise the soft keyboard
onTouchEnd() {
if (!this.dragPoint) {
this.touchStart = null
this.toggle()
return
}

const corner = this.corner
this.touchStart = null
this.dragPoint = null
this.$emit('move', dropControl(this.mode, this.position, corner.x, corner.y, this.frame))
this.wake()
}

onTouchCancel() {
this.touchStart = null
this.dragPoint = null
}
}
</script>
Loading
Loading