App

The value a RocRay app folds in once per cycle, and the small records that ride on it.

Input(msg) is what the platform hands update!: the device snapshot, the window geometry, the simulation clock, the task messages that arrived, the recording status, and the files dropped onto the window. Everything here is pure data with pure receivers, so a package can accept a whole input rather than an open record of the fields it happens to read, and a test can build one from for_tests without a host.

The platform's App re-exports these, so an app writes App.Input(Msg) and never depends on this package directly. msg is the app's own message type; the platform's requires block binds it, which is what makes an Input(msg) parameter the witness that pins a task's return type. Starting a task is an effect and lives in the platform's Task.

default_test_size : { width : I32, height : I32 }

The window size Input.for_tests reports. Ordinary rather than special: a test that depends on the size should say so with with_window.

Dropped : { path : Str, position : { x : F32, y : F32 } }

One file dropped onto the window, and where the pointer was when it landed.

The path is the one the window system reported, absolute on every platform. Reading the file is a separate, waiting effect, so a drop is handled by starting a task:

Task.spawn!(input, || Opened(Files.read_bytes!(drop.path)))

position is the pointer position the host sampled for the cycle the drop arrived on, in the same logical coordinates as input.devices.mouse.position(), so an app that has more than one drop target can tell which one the file landed on.

Input

:= {
    devices : Snapshot,
    window : Snapshot,
    time : Cycle,
    messages : List(msg),
    capture : Status,
    dropped : List(Dropped),
    dropped_overflow : Bool,
}

Everything the host observed for one cycle, handed to update!.

messages contains every task message delivered for this cycle, in the order the tasks finished. Independent tasks may finish in any order; the order they were spawned in does not constrain it. capture contains the recording status sampled for this cycle.

dropped contains the files dropped onto the window since the previous input, in the order the window system reported them. Like a key press it is an interval event rather than a latest value: it is empty on almost every cycle, and exactly one call to update! sees any given drop. At most 64 paths are delivered per cycle; a single drop carrying more has its extra paths discarded, and dropped_overflow says so.

fields : Input(msg) -> {
    devices : Snapshot,
    window : Snapshot,
    time : Cycle,
    messages : List(msg),
    capture : Status,
    dropped : List(Dropped),
    dropped_overflow : Bool,
}

Return the complete structural input for platform-independent libraries.

from_fields : {
    devices : Snapshot,
    window : Snapshot,
    time : Cycle,
    messages : List(msg),
    capture : Status,
    dropped : List(Dropped),
    dropped_overflow : Bool,
} -> Input(msg)

Build an input by stating every sampled field at once.

This is the from-scratch constructor; for_tests is the one to reach for when only a field or two matters, since it supplies neutral values for the rest.

Pass a structural record written out here. Use fields when reading an existing input and the with_* receivers when changing one field.

for_tests : {  } -> Input(msg)

A neutral input for testing an app's pure update logic from an expect.

Nothing is pressed, the window is an ordinary focused default_test_size, the clock reads zero on its first cycle, no messages arrived, and nothing is recording. Customize it with the with_* receivers, which is what makes a test say only the one thing it is about:

expect
    input = App.Input.for_tests({}).with_devices(Devices.none.with_key_pressed(KeyEscape))
    decide(model, input) == Quit

Building the model this is called with is the other half: every host resource an app can hold has a resource-free stub (Draw.Font.stub, Audio.Sound.stub, Text.Prepared.stub, Assets.Texture.stub, ...), so a Model full of assets can be written down in a pure test.

update! itself is effectful, and an expect cannot call it. Keep the decisions in pure functions -- which message to fold in, whether to quit, what work to start -- and test those; update! is the thin shell that performs them.

with_devices : Input(msg), Snapshot -> Input(msg)

Replace this input's sampled device snapshot. Build one from Devices.none.

with_window : Input(msg), Snapshot -> Input(msg)

Replace this input's sampled window geometry and visibility.

with_time : Input(msg), Cycle -> Input(msg)

Replace this input's clock sample. Use it to drive a second cycle: input.with_time({ ..Time.first_cycle, cycle_count: 1 }).

with_messages : Input(msg), List(msg) -> Input(msg)

Deliver task messages on this input, in the order the tasks finished.

with_message : Input(msg), msg -> Input(msg)

Deliver one more task message on this input, after any already there.

with_capture : Input(msg), Status -> Input(msg)

Replace this input's sampled recording status.

with_dropped : Input(msg), List(Dropped) -> Input(msg)

Deliver files dropped onto the window on this input.

with_dropped_overflow : Input(msg), Bool -> Input(msg)

Say that this cycle's drop carried more paths than the host delivers, which is what input.dropped_overflow reports.