xterm-input-panel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +11 -0
- package/.storybook/preview.ts +15 -0
- package/.storybook/vitest.setup.ts +8 -0
- package/CHANGELOG.md +7 -0
- package/package.json +37 -0
- package/src/brand-icons/claude.png +0 -0
- package/src/brand-icons/codex.png +0 -0
- package/src/brand-icons/gemini.png +0 -0
- package/src/icons.ts +77 -0
- package/src/index.ts +16 -0
- package/src/input-method-tab.stories.ts +131 -0
- package/src/input-method-tab.ts +142 -0
- package/src/input-panel-settings.stories.ts +73 -0
- package/src/input-panel-settings.ts +245 -0
- package/src/input-panel.stories.ts +241 -0
- package/src/input-panel.ts +815 -0
- package/src/pixi-theme.test.ts +58 -0
- package/src/pixi-theme.ts +179 -0
- package/src/platform.ts +14 -0
- package/src/shortcut-pages.ts +204 -0
- package/src/shortcut-tab.ts +543 -0
- package/src/virtual-keyboard-layouts.ts +150 -0
- package/src/virtual-keyboard-tab.stories.ts +390 -0
- package/src/virtual-keyboard-tab.ts +642 -0
- package/src/virtual-trackpad-tab.stories.ts +476 -0
- package/src/virtual-trackpad-tab.ts +556 -0
- package/src/xterm-addon.stories.ts +413 -0
- package/src/xterm-addon.ts +947 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +13 -0
- package/vitest.storybook.config.ts +23 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit'
|
|
2
|
+
import {
|
|
3
|
+
Application,
|
|
4
|
+
CanvasTextMetrics,
|
|
5
|
+
Container,
|
|
6
|
+
FederatedPointerEvent,
|
|
7
|
+
Graphics,
|
|
8
|
+
Text,
|
|
9
|
+
TextStyle,
|
|
10
|
+
} from 'pixi.js'
|
|
11
|
+
import { onThemeChange, resolvePixiTheme, type PixiTheme } from './pixi-theme.js'
|
|
12
|
+
import { detectHostPlatform, type PlatformMode } from './platform.js'
|
|
13
|
+
import { LAYOUTS, type KeyDef, type ModifierKey } from './virtual-keyboard-layouts.js'
|
|
14
|
+
|
|
15
|
+
const KEY_PADDING = 3
|
|
16
|
+
const KEY_RADIUS = 4
|
|
17
|
+
const REPEAT_DELAY_MS = 400
|
|
18
|
+
const REPEAT_INTERVAL_MS = 80
|
|
19
|
+
const SWIPE_SHIFT_THRESHOLD = 14
|
|
20
|
+
const DUAL_LABEL_MIN_SIZE = 18
|
|
21
|
+
const DUAL_LABEL_MIN_RATIO = 0.92
|
|
22
|
+
|
|
23
|
+
interface RenderedKey {
|
|
24
|
+
container: Container
|
|
25
|
+
gfx: Graphics
|
|
26
|
+
primaryText: Text
|
|
27
|
+
secondaryText: Text | null
|
|
28
|
+
def: KeyDef
|
|
29
|
+
row: number
|
|
30
|
+
col: number
|
|
31
|
+
width: number
|
|
32
|
+
height: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface KeyDisplay {
|
|
36
|
+
single: string
|
|
37
|
+
top?: string
|
|
38
|
+
bottom?: string
|
|
39
|
+
topActive: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class VirtualKeyboardTab extends LitElement {
|
|
43
|
+
static get properties() {
|
|
44
|
+
return {
|
|
45
|
+
floating: { type: Boolean },
|
|
46
|
+
platform: { type: String, reflect: true },
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static styles = css`
|
|
51
|
+
:host {
|
|
52
|
+
display: block;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 100%;
|
|
55
|
+
touch-action: none;
|
|
56
|
+
}
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
declare floating: boolean
|
|
60
|
+
declare platform: PlatformMode
|
|
61
|
+
|
|
62
|
+
private _app: Application | null = null
|
|
63
|
+
private _container: Container | null = null
|
|
64
|
+
private _keys: RenderedKey[] = []
|
|
65
|
+
private _modifiers: Record<ModifierKey, boolean> = {
|
|
66
|
+
ctrl: false,
|
|
67
|
+
alt: false,
|
|
68
|
+
meta: false,
|
|
69
|
+
shift: false,
|
|
70
|
+
caps: false,
|
|
71
|
+
}
|
|
72
|
+
private _resizeObserver: ResizeObserver | null = null
|
|
73
|
+
private _theme: PixiTheme = resolvePixiTheme(this)
|
|
74
|
+
private _unsubTheme: (() => void) | null = null
|
|
75
|
+
private _repeatTimer: ReturnType<typeof setTimeout> | null = null
|
|
76
|
+
private _repeatInterval: ReturnType<typeof setInterval> | null = null
|
|
77
|
+
|
|
78
|
+
private _activeKey: RenderedKey | null = null
|
|
79
|
+
private _activeStartY = 0
|
|
80
|
+
private _activeSwipeShift = false
|
|
81
|
+
private _activePointerId: number | null = null
|
|
82
|
+
private _stickyModifierMode = false
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
super()
|
|
86
|
+
this.floating = false
|
|
87
|
+
this.platform = 'auto'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async connectedCallback() {
|
|
91
|
+
super.connectedCallback()
|
|
92
|
+
this._theme = resolvePixiTheme(this)
|
|
93
|
+
this._unsubTheme = onThemeChange((theme) => {
|
|
94
|
+
this._theme = theme
|
|
95
|
+
this._layoutKeys()
|
|
96
|
+
}, this)
|
|
97
|
+
await this.updateComplete
|
|
98
|
+
await this._initPixi()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
disconnectedCallback() {
|
|
102
|
+
super.disconnectedCallback()
|
|
103
|
+
this._cancelRepeat()
|
|
104
|
+
this._resizeObserver?.disconnect()
|
|
105
|
+
this._resizeObserver = null
|
|
106
|
+
this._unsubTheme?.()
|
|
107
|
+
this._unsubTheme = null
|
|
108
|
+
this._app?.destroy()
|
|
109
|
+
this._app = null
|
|
110
|
+
this._container = null
|
|
111
|
+
this._activeKey = null
|
|
112
|
+
this._activePointerId = null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private getRows(): KeyDef[][] {
|
|
116
|
+
const hostPlatform =
|
|
117
|
+
this.platform === 'windows' || this.platform === 'macos' || this.platform === 'common'
|
|
118
|
+
? this.platform
|
|
119
|
+
: detectHostPlatform()
|
|
120
|
+
return LAYOUTS[hostPlatform]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async _initPixi() {
|
|
124
|
+
const host = this.shadowRoot?.querySelector('.pixi-host') as HTMLElement
|
|
125
|
+
if (!host) return
|
|
126
|
+
|
|
127
|
+
const app = new Application()
|
|
128
|
+
await app.init({
|
|
129
|
+
background: this._theme.background,
|
|
130
|
+
backgroundAlpha: this.floating ? 0 : 1,
|
|
131
|
+
antialias: true,
|
|
132
|
+
resolution: window.devicePixelRatio || 1,
|
|
133
|
+
autoDensity: false,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
host.appendChild(app.canvas as HTMLCanvasElement)
|
|
137
|
+
this._app = app
|
|
138
|
+
|
|
139
|
+
const canvas = app.canvas as HTMLCanvasElement
|
|
140
|
+
canvas.style.width = '100%'
|
|
141
|
+
canvas.style.height = '100%'
|
|
142
|
+
canvas.style.display = 'block'
|
|
143
|
+
canvas.addEventListener('contextmenu', (e) => e.preventDefault())
|
|
144
|
+
canvas.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false })
|
|
145
|
+
canvas.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false })
|
|
146
|
+
|
|
147
|
+
const width = host.clientWidth
|
|
148
|
+
const height = host.clientHeight
|
|
149
|
+
if (width > 0 && height > 0) {
|
|
150
|
+
app.renderer.resize(width, height)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._container = new Container()
|
|
154
|
+
app.stage.addChild(this._container)
|
|
155
|
+
app.stage.eventMode = 'static'
|
|
156
|
+
app.stage.hitArea = app.screen
|
|
157
|
+
app.stage.on('globalpointermove', (event: FederatedPointerEvent) => {
|
|
158
|
+
this._onActivePointerMove(event)
|
|
159
|
+
})
|
|
160
|
+
app.stage.on('pointerup', () => {
|
|
161
|
+
this._onActivePointerUp()
|
|
162
|
+
})
|
|
163
|
+
app.stage.on('pointerupoutside', () => {
|
|
164
|
+
this._onActivePointerUp()
|
|
165
|
+
})
|
|
166
|
+
app.stage.on('pointercancel', () => {
|
|
167
|
+
this._onActivePointerUp()
|
|
168
|
+
})
|
|
169
|
+
this._layoutKeys()
|
|
170
|
+
|
|
171
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
172
|
+
const nextWidth = host.clientWidth
|
|
173
|
+
const nextHeight = host.clientHeight
|
|
174
|
+
if (nextWidth > 0 && nextHeight > 0) {
|
|
175
|
+
app.renderer.resize(nextWidth, nextHeight)
|
|
176
|
+
this._layoutKeys()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
this._resizeObserver.observe(host)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private _layoutKeys() {
|
|
183
|
+
const app = this._app
|
|
184
|
+
const container = this._container
|
|
185
|
+
if (!app || !container) return
|
|
186
|
+
|
|
187
|
+
container.removeChildren()
|
|
188
|
+
this._keys = []
|
|
189
|
+
|
|
190
|
+
const rows = this.getRows()
|
|
191
|
+
const width = app.screen.width
|
|
192
|
+
const height = app.screen.height
|
|
193
|
+
const rowCount = rows.length
|
|
194
|
+
const rowHeight = (height - KEY_PADDING * (rowCount + 1)) / rowCount
|
|
195
|
+
|
|
196
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
197
|
+
const row = rows[rowIndex] ?? []
|
|
198
|
+
const totalUnits = row.reduce((sum, def) => sum + (def.w ?? 1), 0)
|
|
199
|
+
const keyUnitWidth = (width - KEY_PADDING * (row.length + 1)) / totalUnits
|
|
200
|
+
let x = KEY_PADDING
|
|
201
|
+
|
|
202
|
+
for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
|
|
203
|
+
const def = row[colIndex]!
|
|
204
|
+
const keyWidth = keyUnitWidth * (def.w ?? 1)
|
|
205
|
+
const y = KEY_PADDING + rowIndex * (rowHeight + KEY_PADDING)
|
|
206
|
+
|
|
207
|
+
const keyContainer = new Container()
|
|
208
|
+
keyContainer.x = x
|
|
209
|
+
keyContainer.y = y
|
|
210
|
+
keyContainer.eventMode = 'static'
|
|
211
|
+
keyContainer.cursor = 'pointer'
|
|
212
|
+
|
|
213
|
+
const gfx = new Graphics()
|
|
214
|
+
keyContainer.addChild(gfx)
|
|
215
|
+
|
|
216
|
+
const primaryText = new Text({
|
|
217
|
+
text: '',
|
|
218
|
+
style: new TextStyle({
|
|
219
|
+
fontFamily: 'ui-monospace, SFMono-Regular, monospace',
|
|
220
|
+
fontSize: 13,
|
|
221
|
+
fill: this._theme.text,
|
|
222
|
+
align: 'center',
|
|
223
|
+
}),
|
|
224
|
+
})
|
|
225
|
+
primaryText.anchor.set(0.5)
|
|
226
|
+
keyContainer.addChild(primaryText)
|
|
227
|
+
|
|
228
|
+
let secondaryText: Text | null = null
|
|
229
|
+
if (
|
|
230
|
+
this._shouldShowDualLabels(keyWidth, rowHeight) &&
|
|
231
|
+
!def.modifier &&
|
|
232
|
+
Boolean(def.shift)
|
|
233
|
+
) {
|
|
234
|
+
primaryText.style = new TextStyle({
|
|
235
|
+
fontFamily: 'ui-monospace, SFMono-Regular, monospace',
|
|
236
|
+
fontSize: 10,
|
|
237
|
+
fill: this._theme.text,
|
|
238
|
+
align: 'center',
|
|
239
|
+
})
|
|
240
|
+
primaryText.x = keyWidth / 2
|
|
241
|
+
primaryText.y = rowHeight * 0.35
|
|
242
|
+
|
|
243
|
+
secondaryText = new Text({
|
|
244
|
+
text: '',
|
|
245
|
+
style: new TextStyle({
|
|
246
|
+
fontFamily: 'ui-monospace, SFMono-Regular, monospace',
|
|
247
|
+
fontSize: 10,
|
|
248
|
+
fill: this._theme.textMuted,
|
|
249
|
+
align: 'center',
|
|
250
|
+
}),
|
|
251
|
+
})
|
|
252
|
+
secondaryText.anchor.set(0.5)
|
|
253
|
+
secondaryText.x = keyWidth / 2
|
|
254
|
+
secondaryText.y = rowHeight * 0.74
|
|
255
|
+
keyContainer.addChild(secondaryText)
|
|
256
|
+
} else {
|
|
257
|
+
primaryText.x = keyWidth / 2
|
|
258
|
+
primaryText.y = rowHeight / 2
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
container.addChild(keyContainer)
|
|
262
|
+
|
|
263
|
+
const rendered: RenderedKey = {
|
|
264
|
+
container: keyContainer,
|
|
265
|
+
gfx,
|
|
266
|
+
primaryText,
|
|
267
|
+
secondaryText,
|
|
268
|
+
def,
|
|
269
|
+
row: rowIndex,
|
|
270
|
+
col: colIndex,
|
|
271
|
+
width: keyWidth,
|
|
272
|
+
height: rowHeight,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this._updateKeyVisual(rendered, {
|
|
276
|
+
pressed: false,
|
|
277
|
+
forceShift: false,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
keyContainer.on('pointerdown', (e: FederatedPointerEvent) => {
|
|
281
|
+
this._onKeyDown(rendered, e)
|
|
282
|
+
})
|
|
283
|
+
keyContainer.on('pointermove', (e: FederatedPointerEvent) => {
|
|
284
|
+
this._onKeyMove(rendered, e)
|
|
285
|
+
})
|
|
286
|
+
keyContainer.on('pointerup', () => this._onKeyUp(rendered))
|
|
287
|
+
keyContainer.on('pointerupoutside', () => this._onKeyUp(rendered))
|
|
288
|
+
keyContainer.on('pointerleave', () => this._onKeyLeave(rendered))
|
|
289
|
+
|
|
290
|
+
this._keys.push(rendered)
|
|
291
|
+
x += keyWidth + KEY_PADDING
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private _drawKey(
|
|
297
|
+
gfx: Graphics,
|
|
298
|
+
width: number,
|
|
299
|
+
height: number,
|
|
300
|
+
options: {
|
|
301
|
+
pressed: boolean
|
|
302
|
+
modifier: boolean
|
|
303
|
+
modifierActive: boolean
|
|
304
|
+
forceShift: boolean
|
|
305
|
+
}
|
|
306
|
+
) {
|
|
307
|
+
const { pressed, modifier, modifierActive, forceShift } = options
|
|
308
|
+
const bg = pressed
|
|
309
|
+
? this._theme.keyPressed
|
|
310
|
+
: modifier
|
|
311
|
+
? modifierActive
|
|
312
|
+
? this._theme.keyPressed
|
|
313
|
+
: this._theme.keyModifier
|
|
314
|
+
: this._theme.keyNormal
|
|
315
|
+
|
|
316
|
+
gfx.clear()
|
|
317
|
+
gfx.roundRect(0, 0, width, height, KEY_RADIUS)
|
|
318
|
+
gfx.fill({ color: bg })
|
|
319
|
+
gfx.stroke({
|
|
320
|
+
color: forceShift ? this._theme.accent : this._theme.surfaceBorder,
|
|
321
|
+
width: forceShift ? 1.5 : 1,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private _measureTextWidth(text: Text): number {
|
|
326
|
+
const content = typeof text.text === 'string' ? text.text : String(text.text ?? '')
|
|
327
|
+
if (!content) return 0
|
|
328
|
+
try {
|
|
329
|
+
return CanvasTextMetrics.measureText(content, text.style).width
|
|
330
|
+
} catch {
|
|
331
|
+
return text.width / Math.max(text.scale.x, 0.0001)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private _fitText(text: Text, maxWidth: number, minScale = 0.45) {
|
|
336
|
+
if (maxWidth <= 0) return
|
|
337
|
+
text.scale.set(1)
|
|
338
|
+
const width = this._measureTextWidth(text)
|
|
339
|
+
if (width <= maxWidth) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const scale = Math.max(minScale, maxWidth / width)
|
|
343
|
+
text.scale.set(scale)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private _isLetterKey(def: KeyDef): boolean {
|
|
347
|
+
return def.data.length === 1 && /[a-z]/i.test(def.data)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private _shouldShowDualLabels(keyWidth: number, keyHeight: number): boolean {
|
|
351
|
+
if (keyHeight < DUAL_LABEL_MIN_SIZE) {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
return keyHeight / keyWidth >= DUAL_LABEL_MIN_RATIO
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private _isShifted(def: KeyDef, forceShift: boolean): boolean {
|
|
358
|
+
const baseShift = this._modifiers.shift || forceShift
|
|
359
|
+
if (this._isLetterKey(def) && this._modifiers.caps) {
|
|
360
|
+
return !baseShift
|
|
361
|
+
}
|
|
362
|
+
return baseShift
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private _resolveDisplay(def: KeyDef, forceShift: boolean): KeyDisplay {
|
|
366
|
+
if (this._isLetterKey(def)) {
|
|
367
|
+
const upper = def.shift?.label ?? def.label.toUpperCase()
|
|
368
|
+
const lower = def.label.toLowerCase()
|
|
369
|
+
const topActive = this._isShifted(def, forceShift)
|
|
370
|
+
return {
|
|
371
|
+
single: topActive ? upper : lower,
|
|
372
|
+
top: upper,
|
|
373
|
+
bottom: lower,
|
|
374
|
+
topActive,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (def.shift) {
|
|
379
|
+
const topActive = this._isShifted(def, forceShift)
|
|
380
|
+
return {
|
|
381
|
+
single: topActive ? def.shift.label : def.label,
|
|
382
|
+
top: def.shift.label,
|
|
383
|
+
bottom: def.label,
|
|
384
|
+
topActive,
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
single: def.label,
|
|
390
|
+
topActive: false,
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private _updateKeyVisual(
|
|
395
|
+
rendered: RenderedKey,
|
|
396
|
+
options: {
|
|
397
|
+
pressed: boolean
|
|
398
|
+
forceShift: boolean
|
|
399
|
+
}
|
|
400
|
+
) {
|
|
401
|
+
const { def, primaryText, secondaryText, gfx, width, height } = rendered
|
|
402
|
+
const modifier = Boolean(def.modifier || def.special === 'chord')
|
|
403
|
+
const modifierActive = def.modifier
|
|
404
|
+
? this._modifiers[def.modifier]
|
|
405
|
+
: def.special === 'chord'
|
|
406
|
+
? this._stickyModifierMode
|
|
407
|
+
: false
|
|
408
|
+
const display = this._resolveDisplay(def, options.forceShift)
|
|
409
|
+
|
|
410
|
+
this._drawKey(gfx, width, height, {
|
|
411
|
+
pressed: options.pressed,
|
|
412
|
+
modifier,
|
|
413
|
+
modifierActive,
|
|
414
|
+
forceShift: options.forceShift,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
if (secondaryText && display.top && display.bottom) {
|
|
418
|
+
primaryText.text = display.top
|
|
419
|
+
secondaryText.text = display.bottom
|
|
420
|
+
this._fitText(primaryText, width - 8)
|
|
421
|
+
this._fitText(secondaryText, width - 8)
|
|
422
|
+
|
|
423
|
+
if (options.pressed) {
|
|
424
|
+
primaryText.style.fill = this._theme.accentFg
|
|
425
|
+
secondaryText.style.fill = this._theme.accentFg
|
|
426
|
+
} else if (display.topActive) {
|
|
427
|
+
primaryText.style.fill = this._theme.text
|
|
428
|
+
secondaryText.style.fill = this._theme.textMuted
|
|
429
|
+
} else {
|
|
430
|
+
primaryText.style.fill = this._theme.textMuted
|
|
431
|
+
secondaryText.style.fill = this._theme.text
|
|
432
|
+
}
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
primaryText.text = display.single
|
|
437
|
+
this._fitText(primaryText, width - 8)
|
|
438
|
+
if (options.pressed) {
|
|
439
|
+
primaryText.style.fill = this._theme.accentFg
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (modifierActive) {
|
|
444
|
+
primaryText.style.fill = this._theme.accent
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
primaryText.style.fill = this._theme.text
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private _canSwipeShift(def: KeyDef): boolean {
|
|
452
|
+
return !def.modifier && (Boolean(def.shift) || this._isLetterKey(def))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private _onKeyDown(rendered: RenderedKey, event: FederatedPointerEvent) {
|
|
456
|
+
const { def } = rendered
|
|
457
|
+
|
|
458
|
+
if (def.special === 'chord') {
|
|
459
|
+
this._stickyModifierMode = !this._stickyModifierMode
|
|
460
|
+
if (!this._stickyModifierMode) {
|
|
461
|
+
this._resetTransientModifiers()
|
|
462
|
+
}
|
|
463
|
+
this._vibrate(10)
|
|
464
|
+
this._layoutKeys()
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (def.modifier) {
|
|
469
|
+
this._modifiers[def.modifier] = !this._modifiers[def.modifier]
|
|
470
|
+
this._vibrate(10)
|
|
471
|
+
this._layoutKeys()
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this._activeKey = rendered
|
|
476
|
+
this._setPointerCapture(event.pointerId)
|
|
477
|
+
this._activeStartY = event.global?.y ?? 0
|
|
478
|
+
this._activeSwipeShift = false
|
|
479
|
+
|
|
480
|
+
this._updateKeyVisual(rendered, { pressed: true, forceShift: false })
|
|
481
|
+
this._vibrate(5)
|
|
482
|
+
|
|
483
|
+
this._cancelRepeat()
|
|
484
|
+
this._repeatTimer = setTimeout(() => {
|
|
485
|
+
this._repeatTimer = null
|
|
486
|
+
this._repeatInterval = setInterval(() => {
|
|
487
|
+
this._sendKey(def, this._activeSwipeShift)
|
|
488
|
+
this._vibrate(3)
|
|
489
|
+
}, REPEAT_INTERVAL_MS)
|
|
490
|
+
}, REPEAT_DELAY_MS)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private _onActivePointerMove(event: FederatedPointerEvent) {
|
|
494
|
+
const activeKey = this._activeKey
|
|
495
|
+
if (!activeKey) return
|
|
496
|
+
this._onKeyMove(activeKey, event)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private _onActivePointerUp() {
|
|
500
|
+
const activeKey = this._activeKey
|
|
501
|
+
if (!activeKey) return
|
|
502
|
+
this._onKeyUp(activeKey)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private _onKeyMove(rendered: RenderedKey, event: FederatedPointerEvent) {
|
|
506
|
+
if (this._activeKey !== rendered) return
|
|
507
|
+
if (!this._canSwipeShift(rendered.def)) return
|
|
508
|
+
|
|
509
|
+
const pointerY = event.global?.y ?? this._activeStartY
|
|
510
|
+
const nextSwipeShift = this._activeStartY - pointerY >= SWIPE_SHIFT_THRESHOLD
|
|
511
|
+
if (nextSwipeShift === this._activeSwipeShift) return
|
|
512
|
+
|
|
513
|
+
this._activeSwipeShift = nextSwipeShift
|
|
514
|
+
this._updateKeyVisual(rendered, {
|
|
515
|
+
pressed: true,
|
|
516
|
+
forceShift: this._activeSwipeShift,
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private _onKeyLeave(rendered: RenderedKey) {
|
|
521
|
+
if (this._activeKey !== rendered) return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private _resetTransientModifiers() {
|
|
525
|
+
if (this._stickyModifierMode) {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
this._modifiers.ctrl = false
|
|
529
|
+
this._modifiers.alt = false
|
|
530
|
+
this._modifiers.meta = false
|
|
531
|
+
this._modifiers.shift = false
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private _onKeyUp(rendered: RenderedKey) {
|
|
535
|
+
this._cancelRepeat()
|
|
536
|
+
|
|
537
|
+
if (rendered.def.modifier) return
|
|
538
|
+
if (this._activeKey !== rendered) return
|
|
539
|
+
|
|
540
|
+
const forceShift = this._activeSwipeShift
|
|
541
|
+
this._activeKey = null
|
|
542
|
+
this._activeSwipeShift = false
|
|
543
|
+
|
|
544
|
+
this._updateKeyVisual(rendered, {
|
|
545
|
+
pressed: false,
|
|
546
|
+
forceShift: false,
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
this._sendKey(rendered.def, forceShift)
|
|
550
|
+
this._resetTransientModifiers()
|
|
551
|
+
this._layoutKeys()
|
|
552
|
+
this._releasePointerCapture()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private _resolveOutputData(def: KeyDef, forceShift: boolean): string {
|
|
556
|
+
if (this._isLetterKey(def)) {
|
|
557
|
+
const upper = def.shift?.data ?? def.data.toUpperCase()
|
|
558
|
+
const lower = def.data.toLowerCase()
|
|
559
|
+
return this._isShifted(def, forceShift) ? upper : lower
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (this._isShifted(def, forceShift) && def.shift) {
|
|
563
|
+
return def.shift.data
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return def.data
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private _sendKey(def: KeyDef, forceShift: boolean) {
|
|
570
|
+
let data = this._resolveOutputData(def, forceShift)
|
|
571
|
+
if (!data) return
|
|
572
|
+
|
|
573
|
+
if (this._modifiers.ctrl && data.length === 1) {
|
|
574
|
+
const code = data.toUpperCase().charCodeAt(0) - 64
|
|
575
|
+
if (code > 0 && code < 32) {
|
|
576
|
+
data = String.fromCharCode(code)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (this._modifiers.alt || this._modifiers.meta) {
|
|
581
|
+
data = `\x1b${data}`
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
this.dispatchEvent(
|
|
585
|
+
new CustomEvent('input-panel:send', {
|
|
586
|
+
detail: { data },
|
|
587
|
+
bubbles: true,
|
|
588
|
+
composed: true,
|
|
589
|
+
})
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private _cancelRepeat() {
|
|
594
|
+
if (this._repeatTimer) {
|
|
595
|
+
clearTimeout(this._repeatTimer)
|
|
596
|
+
this._repeatTimer = null
|
|
597
|
+
}
|
|
598
|
+
if (this._repeatInterval) {
|
|
599
|
+
clearInterval(this._repeatInterval)
|
|
600
|
+
this._repeatInterval = null
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private _setPointerCapture(pointerId: number) {
|
|
605
|
+
this._activePointerId = pointerId
|
|
606
|
+
const canvas = this._app?.canvas as HTMLCanvasElement | undefined
|
|
607
|
+
if (!canvas?.setPointerCapture) return
|
|
608
|
+
try {
|
|
609
|
+
canvas.setPointerCapture(pointerId)
|
|
610
|
+
} catch {
|
|
611
|
+
// ignore pointer capture failures
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private _releasePointerCapture() {
|
|
616
|
+
const pointerId = this._activePointerId
|
|
617
|
+
this._activePointerId = null
|
|
618
|
+
if (pointerId == null) return
|
|
619
|
+
const canvas = this._app?.canvas as HTMLCanvasElement | undefined
|
|
620
|
+
if (!canvas?.releasePointerCapture) return
|
|
621
|
+
try {
|
|
622
|
+
if (!canvas.hasPointerCapture?.(pointerId)) return
|
|
623
|
+
canvas.releasePointerCapture(pointerId)
|
|
624
|
+
} catch {
|
|
625
|
+
// ignore pointer capture failures
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private _vibrate(ms: number) {
|
|
630
|
+
try {
|
|
631
|
+
navigator.vibrate?.(ms)
|
|
632
|
+
} catch {
|
|
633
|
+
// ignore
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
render() {
|
|
638
|
+
return html`<div class="pixi-host" style="width:100%;height:100%"></div>`
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
customElements.define('virtual-keyboard-tab', VirtualKeyboardTab)
|