import * as z from "zod/mini"; import { widgetFrameAppearanceSchema, widgetLayoutTransitionScheduleSchema, } from "../host/host-event-schemas.js"; import type { WidgetLayoutTransitionPolicy } from "../host/widget-layout-transition-plan.js"; import { createGeneration, type Generation, generationSchema, } from "./v3-generation.js"; import { VIEW_HOST_PLACEMENTS, type ViewHostPlacement, } from "./view-host-bootstrap.js"; const canonicalIdStringSchema = z.string().check(z.trim(), z.minLength(1)); const opaqueNonBlankStringSchema = z.string().check( z.refine((value) => value.trim().length > 0, { message: "Expected a non-blank string.", }), ); const finiteNumberSchema = z.number().check( z.refine(Number.isFinite, { message: "Expected a finite number.", }), ); const cubicBezierXControlPointSchema = finiteNumberSchema.check( z.minimum(0), z.maximum(1), ); const widgetLayoutCubicBezierSchema = z.tuple([ cubicBezierXControlPointSchema, finiteNumberSchema, cubicBezierXControlPointSchema, finiteNumberSchema, ]); const instantWidgetLayoutTransitionPolicySchema = z.strictObject({ kind: z.literal("instant"), durationMs: z.literal(0), }); const timedWidgetLayoutTransitionPolicySchema = z.strictObject({ kind: z.literal("timed"), durationMs: finiteNumberSchema.check(z.positive()), easing: widgetLayoutCubicBezierSchema, }); const widgetLayoutTransitionPolicySchema: z.ZodMiniType = z.discriminatedUnion("kind", [ instantWidgetLayoutTransitionPolicySchema, timedWidgetLayoutTransitionPolicySchema, ]); const viewHostControlMessageEnvelopeSchema = z.object({ t: z.string().check(z.startsWith("getuserfeedback:")), gen: generationSchema, viewId: z.optional(canonicalIdStringSchema), }); export const viewPlacementSchema: z.ZodMiniType = z.enum(VIEW_HOST_PLACEMENTS); export type ViewPlacement = ViewHostPlacement; /** * View -> host: the view identifies the generation and view it was bootstrapped * with. Receiving this message establishes the view channel, but does not imply * that the view has measured renderable content. */ export const actionViewConnectSchema = z.extend( viewHostControlMessageEnvelopeSchema, { t: z.literal("getuserfeedback:action:viewConnect"), viewId: canonicalIdStringSchema, }, ); export type ActionViewConnect = z.output; /** * View -> host -> core: the view reports renderable geometry. `viewId` is * optional on the view-originated message because a channel-owning host may * stamp it before forwarding the message to core. When the host supplied a * viewport revision, the view echoes that opaque token only on measurements * made from the corresponding viewport constraints. */ export const eventViewSizeSchema = z.extend( viewHostControlMessageEnvelopeSchema, { t: z.literal("getuserfeedback:event:viewSize"), width: z.number().check(z.positive()), height: z.number().check(z.positive()), intrinsicWidth: z.optional(z.number().check(z.positive())), viewportRevision: z.optional(opaqueNonBlankStringSchema), frameAppearance: z.optional(widgetFrameAppearanceSchema), placement: z.optional(viewPlacementSchema), transitionPolicy: z.optional(widgetLayoutTransitionPolicySchema), layoutTransactionId: z.optional(canonicalIdStringSchema), }, ); export type EventViewSize = z.output; export function buildEventViewSize(input: { gen: number | Generation; viewId?: string; width: number; height: number; intrinsicWidth?: number; viewportRevision?: string; frameAppearance?: EventViewSize["frameAppearance"]; placement?: EventViewSize["placement"]; transitionPolicy?: EventViewSize["transitionPolicy"]; layoutTransactionId?: string; }): EventViewSize { return { t: "getuserfeedback:event:viewSize", gen: typeof input.gen === "number" ? createGeneration(input.gen) : input.gen, viewId: input.viewId, width: input.width, height: input.height, intrinsicWidth: input.intrinsicWidth, viewportRevision: input.viewportRevision, frameAppearance: input.frameAppearance, placement: input.placement, transitionPolicy: input.transitionPolicy, layoutTransactionId: input.layoutTransactionId, }; } /** * Host -> View: the host reports its viewport constraints for the view to * adjust against. `viewportRevision` is an optional opaque host token that the * view echoes on measurements made from these constraints. */ export const eventViewportSizeSchema = z.extend( viewHostControlMessageEnvelopeSchema, { t: z.literal("getuserfeedback:event:viewportSize"), viewId: canonicalIdStringSchema, width: z.number().check(z.positive()), height: z.number().check(z.positive()), maxWidth: z.number().check(z.positive()), maxHeight: z.number().check(z.positive()), viewportRevision: z.optional(opaqueNonBlankStringSchema), padding: z.object({ x: z.number().check(z.int(), z.minimum(0)), y: z.number().check(z.int(), z.minimum(0)), }), }, ); export type EventViewportSize = z.output; export function buildEventViewportSize(input: { gen: number | Generation; viewId: string; width: number; height: number; maxWidth: number; maxHeight: number; viewportRevision?: string; padding: { x: number; y: number }; }): EventViewportSize { return { t: "getuserfeedback:event:viewportSize", gen: typeof input.gen === "number" ? createGeneration(input.gen) : input.gen, viewId: input.viewId, width: input.width, height: input.height, maxWidth: input.maxWidth, maxHeight: input.maxHeight, viewportRevision: input.viewportRevision, padding: input.padding, }; } /** * Host -> View: the host confirms that its side of a view layout transaction * has committed, allowing the view to commit the matching layout state. An * optional schedule coordinates their timed transitions. */ export const eventLayoutTransactionCommittedSchema = z.extend( viewHostControlMessageEnvelopeSchema, { t: z.literal("getuserfeedback:event:layoutTransactionCommitted"), viewId: canonicalIdStringSchema, layoutTransactionId: canonicalIdStringSchema, transitionSchedule: z.optional(widgetLayoutTransitionScheduleSchema), }, ); export type EventLayoutTransactionCommitted = z.output< typeof eventLayoutTransactionCommittedSchema >; export function buildEventLayoutTransactionCommitted(input: { gen: number | Generation; viewId: string; layoutTransactionId: string; transitionSchedule?: EventLayoutTransactionCommitted["transitionSchedule"]; }): EventLayoutTransactionCommitted { return { t: "getuserfeedback:event:layoutTransactionCommitted", gen: typeof input.gen === "number" ? createGeneration(input.gen) : input.gen, viewId: input.viewId, layoutTransactionId: input.layoutTransactionId, ...(input.transitionSchedule === undefined ? {} : { transitionSchedule: input.transitionSchedule }), }; } /** * View -> host -> core: the embedded view requests closure. As with view size, * a channel-owning host may stamp `viewId` while forwarding the message. */ export const actionRequestCloseViewSchema = z.extend( viewHostControlMessageEnvelopeSchema, { t: z.literal("getuserfeedback:action:requestCloseView"), }, ); export type ActionRequestCloseView = z.output< typeof actionRequestCloseViewSchema >;