a33d579177
* fix(macos): preserve file drop targets - Route AppKit drops with labeled, view-local coordinates across canvas, WebView, and window fallback paths. - Preserve drop metadata through Zig and cover widget routing, TypeScript contracts, docs, and examples. Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com> * fix(macos): preserve file drop coordinates * fix: keep file drop targets sized * fix: keep capability webview interactive * fix: constrain capabilities window width --------- Co-authored-by: Mohak Bajaj <77928693+MohakBajaj@users.noreply.github.com>
355 lines
15 KiB
TypeScript
355 lines
15 KiB
TypeScript
// @native-sdk/core/events — the canonical event record types the host and
|
|
// markup deliver into an app core, an SDK LIBRARY module: pure type
|
|
// declarations in the app-core subset, emitted into your core when imported
|
|
// and absent when not. Under node the same file resolves as-is.
|
|
//
|
|
// Every record here matches, field for field, the structural shape a
|
|
// runtime matcher expects — markup's `on-input`/`on-scroll` mirrors
|
|
// (`declaredTextInputUnion`/`declaredScrollStateRecord`), the generated
|
|
// wiring's channel builders (`frameMsg`/`keyMsg`/`pinchMsg`/`dropMsg`/
|
|
// `appearanceMsg`/`chromeMsg`), and the audio event arm validator. Matching stays
|
|
// STRUCTURAL either way (type identity cannot cross the emission
|
|
// boundary), so declaring the same shape in your own core remains legal;
|
|
// these exports exist so no core has to re-type the vocabulary and no
|
|
// hand-rolled mirror can drift. One home per type: the text-input family
|
|
// lives with the byte-splice engine in `@native-sdk/core/text` and is
|
|
// re-exported here, so `@native-sdk/core/events` resolves every event
|
|
// record an app binds. (Importing this module pulls `./text.ts` into the
|
|
// emitted core with it; unused engine functions cost source lines, never
|
|
// binary — Zig compiles only what the core references.)
|
|
//
|
|
// Names are unique across a core's whole module graph (NS1038): a core
|
|
// that imports one of these types cannot also declare its own record
|
|
// under the same name — delete the in-file mirror and import it instead.
|
|
//
|
|
// How each type is bound:
|
|
// - `TextInputEvent`, `ScrollState`: Msg-arm PAYLOAD fields —
|
|
// `{ kind: "draft_edit"; edit: TextInputEvent }`,
|
|
// `{ kind: "scrolled"; scroll: ScrollState }`.
|
|
// - `FrameEvent`, `KeyEvent`, `PinchEvent`, `FileDropEvent`: the wiring channels'
|
|
// parameter records — `frameMsg(model, frame: FrameEvent)`,
|
|
// `keyMsg(key: KeyEvent)`, `pinchMsg(pinch: PinchEvent)`,
|
|
// `dropMsg(drop: FileDropEvent)`.
|
|
// - `ColorScheme`, `ChromeInsets`, `ChromeButtons`, `AudioState`: field
|
|
// types INSIDE the arm records the `appearanceMsg`/`chromeMsg`/audio
|
|
// routes name (the arms themselves stay inline unions of `kind` plus
|
|
// the event's fields — the subset has no intersection arms).
|
|
// - `AppearanceEvent`, `ChromeEvent`, `AudioEvent`: the full arm
|
|
// payload shapes, canonical and importable for helper signatures
|
|
// (an arm value is structurally assignable to its event record).
|
|
// - `StatusItemState`, `StatusItemDescriptor`, and their nested records:
|
|
// the model-derived shell returned by `statusItem(model)` or keyed
|
|
// `statusItems(model)`; the generated launcher refreshes shell,
|
|
// presentation, visibility, and menu after committed model changes.
|
|
|
|
export type { TextCaretDirection, TextCaretMove, TextSelection, TextInputEvent } from "./text.ts";
|
|
|
|
/// One row in a menu-bar status item's menu. Non-separator rows need a
|
|
/// unique non-zero `id`, a non-empty `label`, and (when actionable) a
|
|
/// command name accepted by `commandMsg`. Rich readout roles may carry
|
|
/// secondary `detail`; command rows may carry a key equivalent. Spell
|
|
/// every field explicitly because app-core records have one exact shape.
|
|
/// A status item may expose at most 32 rows.
|
|
export type StatusItemTone = "normal" | "warning" | "critical";
|
|
|
|
export type StatusItemMenuRole = "command" | "info" | "header" | "hero" | "agent" | "context";
|
|
|
|
export interface StatusItemModifiers {
|
|
readonly primary: boolean;
|
|
readonly command: boolean;
|
|
readonly control: boolean;
|
|
readonly option: boolean;
|
|
readonly shift: boolean;
|
|
}
|
|
|
|
export interface StatusItemPresentation {
|
|
readonly title: Uint8Array;
|
|
readonly width: number;
|
|
readonly tone: StatusItemTone;
|
|
readonly iconOpacity: number;
|
|
readonly monospaced: boolean;
|
|
}
|
|
|
|
export interface StatusItemMenuItem {
|
|
readonly id: number;
|
|
readonly label: Uint8Array;
|
|
readonly command: Uint8Array;
|
|
readonly separator: boolean;
|
|
readonly enabled: boolean;
|
|
readonly detail: Uint8Array;
|
|
readonly role: StatusItemMenuRole;
|
|
readonly key: Uint8Array;
|
|
readonly modifiers: StatusItemModifiers;
|
|
}
|
|
|
|
/// The generated launcher's model-derived menu-bar status item. Export
|
|
/// `statusItem(model: Model): StatusItemState` from `src/core.ts`; its
|
|
/// shell, presentation, and rows are re-derived after every committed model
|
|
/// update. Byte slices are borrowed only for that apply, so returning literals
|
|
/// or freshly built arrays is safe.
|
|
export interface StatusItemState {
|
|
readonly iconPath: Uint8Array;
|
|
readonly tooltip: Uint8Array;
|
|
readonly activationCommand: Uint8Array;
|
|
readonly alternateActivationCommand: Uint8Array;
|
|
readonly openCommand: Uint8Array;
|
|
readonly presentation: StatusItemPresentation;
|
|
readonly items: readonly StatusItemMenuItem[];
|
|
}
|
|
|
|
/// One independently reconciled menu-bar status item. Export
|
|
/// `statusItems(model: Model): readonly StatusItemDescriptor[]`; `id`
|
|
/// preserves the native item while all remaining fields update live.
|
|
export interface StatusItemDescriptor {
|
|
readonly id: number;
|
|
readonly visible: boolean;
|
|
readonly iconPath: Uint8Array;
|
|
readonly tooltip: Uint8Array;
|
|
readonly activationCommand: Uint8Array;
|
|
readonly alternateActivationCommand: Uint8Array;
|
|
readonly openCommand: Uint8Array;
|
|
readonly presentation: StatusItemPresentation;
|
|
readonly items: readonly StatusItemMenuItem[];
|
|
}
|
|
|
|
/// What the user's close affordance does for a model-declared secondary
|
|
/// window. `quit` really closes it and routes `onCloseCommand`; `hide` keeps
|
|
/// the native window and view tree alive for `Cmd.showWindow(label)`.
|
|
export type WindowClosePolicy = "quit" | "hide";
|
|
|
|
export type WindowTitlebarStyle = "standard" | "hidden_inset" | "hidden_inset_tall" | "chromeless";
|
|
|
|
/// Author-facing input to `windowDescriptor`; omitted fields receive the
|
|
/// same defaults as UiApp.WindowDescriptor.
|
|
export interface WindowDescriptorSpec {
|
|
readonly label: Uint8Array;
|
|
readonly canvasLabel: Uint8Array;
|
|
readonly title?: Uint8Array;
|
|
readonly width?: number;
|
|
readonly height?: number;
|
|
readonly x?: number | null;
|
|
readonly y?: number | null;
|
|
readonly resizable?: boolean;
|
|
readonly minWidth?: number;
|
|
readonly minHeight?: number;
|
|
readonly titlebar?: WindowTitlebarStyle;
|
|
readonly transparent?: boolean;
|
|
readonly alwaysOnTop?: boolean;
|
|
readonly clickThrough?: boolean;
|
|
readonly activateOnShow?: boolean;
|
|
readonly allowsFullscreen?: boolean;
|
|
readonly closePolicy?: WindowClosePolicy;
|
|
readonly onCloseCommand?: Uint8Array;
|
|
}
|
|
|
|
/// One independently reconciled secondary window returned by
|
|
/// `windows(model)`. The generated launcher compiles
|
|
/// `src/windows/<label>.native` as its view; presence is liveness. Construct
|
|
/// entries with `windowDescriptor` and spell `label` as a literal
|
|
/// `asciiBytes("<label>")` so check/build can prove that root exists.
|
|
export interface WindowDescriptor {
|
|
readonly label: Uint8Array;
|
|
readonly canvasLabel: Uint8Array;
|
|
readonly title: Uint8Array;
|
|
readonly width: number;
|
|
readonly height: number;
|
|
readonly x: number | null;
|
|
readonly y: number | null;
|
|
readonly resizable: boolean;
|
|
readonly minWidth: number;
|
|
readonly minHeight: number;
|
|
readonly titlebar: WindowTitlebarStyle;
|
|
readonly transparent: boolean;
|
|
readonly alwaysOnTop: boolean;
|
|
readonly clickThrough: boolean;
|
|
readonly activateOnShow: boolean;
|
|
readonly allowsFullscreen: boolean;
|
|
readonly closePolicy: WindowClosePolicy;
|
|
readonly onCloseCommand: Uint8Array;
|
|
}
|
|
|
|
/// The scroll-state mirror markup's `on-scroll` matches structurally: a
|
|
/// record of exactly these eight numeric fields — the TWO-AXIS shape, one
|
|
/// offset/velocity/viewport/content quartet per axis. Offsets and extents
|
|
/// are canvas points; velocities are points per second while a fling
|
|
/// decays. A vertical list carries live `...Y` fields and quiet `...X`
|
|
/// ones (offset 0, content pinned to the viewport width); a horizontal
|
|
/// shelf the reverse. Echo `offsetY` into the model field bound as the
|
|
/// scroll's `value` (and `offsetX` into `value-x` on horizontal-capable
|
|
/// regions) to keep the region model-driven — setting those fields in
|
|
/// update scrolls it.
|
|
export interface ScrollState {
|
|
readonly offsetX: number;
|
|
readonly offsetY: number;
|
|
readonly velocityX: number;
|
|
readonly velocityY: number;
|
|
readonly viewportExtentX: number;
|
|
readonly viewportExtentY: number;
|
|
readonly contentExtentX: number;
|
|
readonly contentExtentY: number;
|
|
}
|
|
|
|
/// The presented-frame channel's record (`frameMsg(model, frame)`): the
|
|
/// canvas size in points plus the frame clock in fractional milliseconds.
|
|
/// Return null from `frameMsg` for frames that change nothing — the idle
|
|
/// law holds exactly when an idle app dispatches nothing.
|
|
export interface FrameEvent {
|
|
readonly width: number;
|
|
readonly height: number;
|
|
readonly timestampMs: number;
|
|
readonly intervalMs: number;
|
|
}
|
|
|
|
/// The key-fallback channel's record (`keyMsg(key)`): the key NAME arrives
|
|
/// lowercased ("space", "arrowleft"), plus the four modifier booleans. A
|
|
/// focused widget's own keys and editable text always win first.
|
|
export interface KeyEvent {
|
|
readonly key: string;
|
|
readonly shift: boolean;
|
|
readonly control: boolean;
|
|
readonly alt: boolean;
|
|
readonly super: boolean;
|
|
}
|
|
|
|
/// The pinch phase vocabulary — a NAMED begin/change/end alias (the host
|
|
/// matches enum members by name, so this is the `phase` field's type in
|
|
/// `pinchMsg`'s parameter record). A host-cancelled gesture folds into
|
|
/// "end": pinch delivers incremental deltas the app applies as they
|
|
/// arrive, so there is no transient state to roll back. A terminal host
|
|
/// event that still measured a nonzero delta delivers it as a final
|
|
/// "change" before the "end", so the cumulative product always matches
|
|
/// what the OS reported.
|
|
export type PinchPhase = "begin" | "change" | "end";
|
|
|
|
/// The pinch channel's record (`pinchMsg(pinch)`): the trackpad pinch
|
|
/// gesture, phase-explicit. `windowId`/`label` name the source window and
|
|
/// gpu-surface view — `x`/`y` are view-local, so a coordinate without its
|
|
/// view is not a position, and multi-window apps tell pinches apart by
|
|
/// these. `scale` is the magnification DELTA for this event (nonzero
|
|
/// only on "change"), and the delta is MULTIPLICATIVE: the cumulative
|
|
/// gesture scale is the running product of `1 + scale` — apply it
|
|
/// memorylessly, `zoom *= 1 + scale`, no gesture-start bookkeeping. On
|
|
/// macOS this is AppKit's raw per-event `NSEvent.magnification`, which
|
|
/// IS that multiplicative delta per the browser-engine convention, so
|
|
/// the product matches the zoom the same gesture performs in Safari and
|
|
/// Chrome. `x`/`y` is the pointer anchor in view-local canvas
|
|
/// points — the pointer location during the gesture (hosts report gesture
|
|
/// events at the pointer, not at a midpoint between the fingers), so a
|
|
/// zoom can anchor under the cursor. Pinch is a view-global gesture — it
|
|
/// never routes through widgets — so this is the honest home for timeline
|
|
/// and canvas zoom. Only hosts with a pinch source emit it (macOS today).
|
|
export interface PinchEvent {
|
|
readonly windowId: number;
|
|
readonly label: string;
|
|
readonly phase: PinchPhase;
|
|
readonly scale: number;
|
|
readonly x: number;
|
|
readonly y: number;
|
|
}
|
|
|
|
/// A file drop's optional point in top-left-origin local coordinates. A
|
|
/// non-empty `FileDropEvent.viewLabel` names its canvas or WebView coordinate
|
|
/// space; an empty label may carry window-content coordinates. Hosts that
|
|
/// cannot resolve local coordinates leave `FileDropEvent.point` null.
|
|
export interface FileDropPoint {
|
|
readonly x: number;
|
|
readonly y: number;
|
|
}
|
|
|
|
/// The app-level file-drop channel's record (`dropMsg(drop)`). `windowId`
|
|
/// and `viewLabel` identify the source; `point` is present when the host can
|
|
/// resolve local coordinates. The macOS system host reports labeled canvas or
|
|
/// WebView coordinates and degrades unlabeled regions to window-content
|
|
/// coordinates; hosts without either leave `point` null. Paths are byte text
|
|
/// so arbitrary UTF-8 filesystem names cross without becoming runtime JS
|
|
/// strings. Return null from `dropMsg` to ignore a drop, or map it to an
|
|
/// ordinary Msg.
|
|
export interface FileDropEvent {
|
|
readonly windowId: number;
|
|
readonly viewLabel: string;
|
|
readonly point: FileDropPoint | null;
|
|
readonly paths: readonly Uint8Array[];
|
|
}
|
|
|
|
/// The appearance vocabulary — a NAMED light/dark alias (the host matches
|
|
/// enum members by name, so this is the `colorScheme` field's type in the
|
|
/// arm `appearanceMsg` names).
|
|
export type ColorScheme = "light" | "dark";
|
|
|
|
/// The appearance arm's payload shape, whole: what the system appearance
|
|
/// channel delivers into the arm `appearanceMsg` names. The arm itself
|
|
/// stays an inline union member carrying exactly these fields plus `kind`.
|
|
export interface AppearanceEvent {
|
|
readonly colorScheme: ColorScheme;
|
|
readonly reduceMotion: boolean;
|
|
readonly highContrast: boolean;
|
|
}
|
|
|
|
/// The chrome arm's insets record: the window-chrome band (hidden-titlebar
|
|
/// geometry) in canvas points.
|
|
export interface ChromeInsets {
|
|
readonly top: number;
|
|
readonly right: number;
|
|
readonly bottom: number;
|
|
readonly left: number;
|
|
}
|
|
|
|
/// The chrome arm's traffic-light record: the window buttons' frame in
|
|
/// canvas points.
|
|
export interface ChromeButtons {
|
|
readonly x: number;
|
|
readonly y: number;
|
|
readonly width: number;
|
|
readonly height: number;
|
|
}
|
|
|
|
/// The chrome arm's payload shape, whole: what the window-chrome channel
|
|
/// delivers into the arm `chromeMsg` names — delivered before the first
|
|
/// view build and again whenever the geometry changes.
|
|
export interface ChromeEvent {
|
|
readonly insets: ChromeInsets;
|
|
readonly buttons: ChromeButtons;
|
|
readonly tabsProjected: boolean;
|
|
}
|
|
|
|
/// The audio event states, mirroring the engine's event vocabulary:
|
|
/// `loaded` acknowledges a successful load with the player's duration
|
|
/// estimate; `position` ticks at the platform's honest cadence (~500ms)
|
|
/// while playing; `completed` fires exactly once at the natural end;
|
|
/// `failed` reports a load/decode/device failure; `rejected` a command the
|
|
/// effects layer refused; `spectrum` carries a band-magnitude analysis
|
|
/// frame from hosts that analyze their playback.
|
|
export type AudioState = "loaded" | "position" | "completed" | "failed" | "rejected" | "spectrum";
|
|
|
|
/// The audio event arm's payload shape, whole — the one SDK-fixed record,
|
|
/// six fields matched by NAME: `positionMs`/`durationMs` are milliseconds,
|
|
/// `playing` is the transport state, `buffering` is true while a streamed
|
|
/// source stalls waiting for bytes, and `bands` is the 32 spectrum band
|
|
/// magnitudes (0..255 each, all zeros outside "spectrum" events).
|
|
export interface AudioEvent {
|
|
readonly state: AudioState;
|
|
readonly positionMs: number;
|
|
readonly durationMs: number;
|
|
readonly playing: boolean;
|
|
readonly buffering: boolean;
|
|
readonly bands: Uint8Array;
|
|
}
|
|
|
|
/// Native audio-capture vocabulary and fixed event record. PCM chunks are
|
|
/// interleaved signed 16-bit little-endian in the requested format.
|
|
export type AudioCaptureSource = "microphone" | "system";
|
|
export type AudioCaptureState = "started" | "data" | "failed" | "stopped" | "rejected";
|
|
|
|
export interface AudioCaptureEvent {
|
|
readonly key: number;
|
|
readonly state: AudioCaptureState;
|
|
readonly source: AudioCaptureSource;
|
|
readonly sampleRate: number;
|
|
readonly channels: number;
|
|
readonly timestampMs: number;
|
|
readonly frames: number;
|
|
readonly pcm: Uint8Array;
|
|
readonly droppedPending: number;
|
|
readonly droppedTotal: number;
|
|
}
|