Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/presentation/vgui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Present generic Source 1 panel trees, controls, resources, schemes, localized te
- Preserve Label-descendant alignment/insets, exact normal/armed/depressed/selected/focused/disabled colors and borders, slider track/thumb state, PropertySheet tabs, SectionedList column flags, and one workspace-positioned ComboBox Menu popup with separate selected, armed and disabled rows.
- Own the generic developer-console frame, pointer-captured title movement and eight-direction resize, bounded output and command history, catalog-driven completion presentation, text-entry interaction, and typed submission/completion/system requests without executing commands or owning convar state.
- Own one bounded client diagnostic panel for immutable FPS and position inputs without owning `cl_showfps`, `cl_showpos`, map, camera, player, or Simulation state.
- Own keyboard, pointer, cursor, capture, focus, navigation, IME, clipboard-seam, accessibility, reduced-motion, browser-resize, and device-pixel-ratio behavior.
- Own keyboard, pointer, cursor, capture, focus, navigation, IME, clipboard-seam, accessibility, reduced-motion, browser-resize, and device-pixel-ratio behavior while leaving keyboard, text-input, and composition events from foreign DOM roots untouched.
- Recompute screen-relative and explicit `proportionalToParent` resource geometry from each admitted integer CSS-pixel viewport; device-pixel ratio alone never changes panel bounds.
- Compose lossless scheme documents, select desktop conditions and ordered font candidates, request exact content/external/bitmap/local sources, mount range faces atomically, consume supplied metrics/raster profiles, and suppress only unavailable glyph paint without disabling panel state or input.
- Present VGUI through direct DOM and CSS without importing Preact.
Expand Down
2 changes: 1 addition & 1 deletion packages/presentation/vgui/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Ownership is resolved. Individual rows remain `Not started` or `Blocked` accordi
| Registered animation variables inherit through control classes, initialize defaults, apply resources, expose typed values, and reject incompatible converters. | Generic registrations inherit base variables plus exact selected-control variables; custom registrations extend them immutably; all 15 converters validate defaults, resources and animation writes before publication. | **Animation-map reflection comparison:** fixed custom maps cover every converter, inherited defaults, resource application, typed writes and incompatible target rejection. | Ready |
| Ordered HUD-animation manifests and scripts define named sequences, conditions, first-declaration collision retention, delays, durations, scoped start, cancellation, and viewport-size reload. | Exact provider composition resolves both manifest dependencies. The generic selected-control implementation consumes typed parsed script sets; the external UI-script format owner and generated occurrence inventory remain absent. | **Parsed-sequence snapshot and virtual-clock comparison:** compare sequence registries, command lists, durations, collision retention, queues, cancellation, and resize results exactly. | In progress |
| All 13 commands, 9 interpolators, 9 built-in variables, 15 converters, and 9 relative alignments in the candidate inventory execute with deterministic queue order and target resolution. | Every generic command, interpolator, built-in variable, converter and relative alignment executes under the injected clock; tests cover mixed cancellation and every runtime identity. Parser token-span diagnostics and parser byte/token budgets remain producer-owned and incomplete. | **Inventory-driven animation differential:** [`tests/runtime.test.ts`](tests/runtime.test.ts) contains all-command, all-interpolator, all-converter and all-alignment vectors. | In progress |
| Browser events enter one input context, edge states last one frame, and routing distinguishes mouse-over, mouse focus, capture, calculated key focus, committed focus, and modal state. | One document/host adapter normalizes pointer, wheel, keyboard, input, composition, blur and visibility events; the model snapshots each distinct state and one-frame edge set. | **Browser-event normalization trace:** fixed synthetic schedules plus headed Chrome evidence compare normalized events and complete input state. | Ready |
| Browser events enter one input context, edge states last one frame, and routing distinguishes mouse-over, mouse focus, capture, calculated key focus, committed focus, and modal state. | One document/host adapter normalizes pointer, wheel, keyboard, input, composition, blur and visibility events. Keyboard, text-input, and composition events enter the runtime only when their DOM target belongs to its host; foreign UI contexts retain browser defaults and cannot mutate runtime input state. The model snapshots each distinct state and one-frame edge set. | **Browser-event normalization trace:** fixed synthetic schedules, foreign editable-control events, and headed Chrome evidence compare default prevention, normalized events, and complete input state. | Ready |
| Key press, key-code typed, Unicode typed, release, repeat, modifier, unhandled-key, and control parent-chain behavior preserve event type and order. | Physical press, semantic typed, Unicode input, release, browser repeat and modifiers retain distinct messages and edge state; selected controls chain handled messages. Explicit unhandled-key listener registration and injected repeat synthesis remain absent. | **Keyboard trace comparison:** selected keyboard, text, focus/default/hotkey and browser schedules compare ordering and state. | In progress |
| Pointer enter, exit, move, press, double and triple press, release, mismatch, wheel, cursor, explicit position, and capture obey hit-test and initiating-button rules. | Pointer routing retains actual mouse-over separately from capture, exit-before-enter, coalesced position, click count, wheel, initiating-button release and browser capture cleanup. Explicit cursor-position requests and mismatch observations remain absent. | **Pointer trace comparison:** overlap, drag, modal, deletion, popup and capture schedules compare targets/order/state. | In progress |
| Tab, hotkey, default-button, directional, delegated, and popup focus navigation skip ineligible panels, remain in scope, and terminate cycles. | Focus groups order eligible direct children by nonzero tab position, wrap once, preserve default/current-default state, delegate labels, resolve named directional relays with cycle termination and prioritize keyboard popups. | **Focus graph model comparison:** tab/default/hotkey/directional, popup and loss-before-gain vectors compare requested and committed focus plus message order. | Ready |
Expand Down
15 changes: 13 additions & 2 deletions packages/presentation/vgui/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5468,6 +5468,7 @@ class SourceVguiRuntime implements VguiRuntime {
}, { passive: false })
this.listen(this.document, "keydown", (raw) => {
const event = raw as KeyboardEvent
if (!this.browserEventBelongsToHost(event)) return
const key = this.browserKey(event.key)
if (["Tab", "Enter", "Space", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End", "Backspace", "Delete"].includes(key)) event.preventDefault()
this.apply({ kind: "key-press", key, shift: event.shiftKey, control: event.ctrlKey, alt: event.altKey, meta: event.metaKey, repeat: event.repeat })
Expand All @@ -5476,23 +5477,29 @@ class SourceVguiRuntime implements VguiRuntime {
})
this.listen(this.document, "keyup", (raw) => {
const event = raw as KeyboardEvent
if (!this.browserEventBelongsToHost(event)) return
this.apply({ kind: "key-release", key: this.browserKey(event.key), shift: event.shiftKey, control: event.ctrlKey, alt: event.altKey, meta: event.metaKey })
this.browserFrame()
})
this.listen(this.document, "input", (raw) => {
const target = raw.target as HTMLInputElement | null
if (!this.browserEventBelongsToHost(raw)) return
const panelId = Number(target?.dataset.vguiPanel)
if (!target || !Number.isSafeInteger(panelId)) return
if (!target || !Number.isSafeInteger(panelId) || this.panels.get(panelId)?.element !== target) return
this.browserInputValue(panelId, target.value)
this.browserFrame()
})
this.listen(this.document, "compositionstart", () => this.browserApply({ kind: "composition-start" }))
this.listen(this.document, "compositionstart", (raw) => {
if (this.browserEventBelongsToHost(raw)) this.browserApply({ kind: "composition-start" })
})
this.listen(this.document, "compositionupdate", (raw) => {
const event = raw as CompositionEvent
if (!this.browserEventBelongsToHost(event)) return
this.browserApply({ kind: "composition-update", text: event.data, caret: event.data.length })
})
this.listen(this.document, "compositionend", (raw) => {
const event = raw as CompositionEvent
if (!this.browserEventBelongsToHost(event)) return
this.browserApply({ kind: "composition-end", text: event.data })
})
this.listen(this.document, "visibilitychange", () => {
Expand Down Expand Up @@ -5521,6 +5528,10 @@ class SourceVguiRuntime implements VguiRuntime {
this.browserFrame()
}

private browserEventBelongsToHost(event: Event): boolean {
return event.target !== null && this.host.contains(event.target as Node)
}

private browserInputValue(panelId: VguiPanelId, value: string): void {
const panel = this.panels.get(panelId)
if (!panel || !panel.editable || panel.compositionActive || !validString(value, this.limits.maxTextCodeUnits)) return
Expand Down
35 changes: 34 additions & 1 deletion packages/presentation/vgui/tests/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type VguiRuntimeLimits,
type VguiScheme,
} from "../src"
import { FakeDocument, createRoot, descendants } from "./fake-dom"
import { FakeDocument, FakeEvent, createRoot, descendants } from "./fake-dom"

const limits: VguiRuntimeLimits = Object.freeze({
maxPanels: 128,
Expand Down Expand Up @@ -151,6 +151,39 @@ function setup(animationScripts = emptyAnimations, customControls: VguiRuntimeCo
}

describe("generic Source VGUI runtime", () => {
test("leaves foreign keyboard and input events to their owning DOM context", () => {
const { document, root, runtime } = setup()
const entry = operation(runtime, {
kind: "create-panel",
parent: 1,
control: "TextEntry",
name: "Entry",
properties: [{ name: "wide", value: "100" }, { name: "tall", value: "24" }],
}).panel!
const foreign = document.createElement("input")
foreign.dataset.vguiPanel = String(entry)
foreign.value = "foreign text"

for (const [key, code] of [[" ", "Space"], ["Backspace", "Backspace"]] as const) {
const event = new FakeEvent("keydown", { key, code })
event.target = foreign
document.dispatchEvent(event)
expect(event.defaultPrevented).toBeFalse()
}
const input = new FakeEvent("input")
input.target = foreign
document.dispatchEvent(input)

expect(runtime.snapshot().input.downKeys).toEqual([])
expect(runtime.snapshot().panels.find((panel) => panel.id === entry)?.text).toBe("")

const owned = descendants(root).find((element) => element.dataset.vguiPanel === String(entry))!
const backspace = new FakeEvent("keydown", { key: "Backspace", code: "Backspace" })
backspace.target = owned
document.dispatchEvent(backspace)
expect(backspace.defaultPrevented).toBeTrue()
})

test("defers presentation and static frames retain mounted DOM", () => {
const { root, runtime, time } = setup()
let panelsDuringBatch = 0
Expand Down