The complete rxx scripting API. Plugins are
Rune scripts; the rx
object passed to every hook is the host interface. New here? Start with the
scripting guide. Every constructor that can fail
returns an Option and posts the error to the message line on
None.
rxx is in early development — expect the script API to change. Report breakage on the issues page.
Click a function to show its parameter types, units, return value,
and failure cases. Click again to collapse it. Method receivers (rx,
encoder, texture, and pass) are not
repeated in parameter lists. () means no return value;
Result<T, String> is Ok(value) or
Err(message).
Plugins are flat <name>.rune files or packages at
<name>/<name>.rune with adjacent assets. Search order:
repeated --plugin-dir <path> arguments, the PATH-style
RXX_PLUGIN_PATH list, local plugins/, then the OS
config directory's plugins/. First match per name wins; plugins
load in lexicographic name order. --isolated-plugins restricts
discovery to explicitly supplied directories.
pub fn init(rx) runs at load and returns the plugin's state
value, which is passed back as the first argument of every hook, command
handler, and export. Script errors and GPU validation errors disable the
plugin and post one message; other plugins are unaffected.
pub fn init(rx) {
rx.register_command("greet", ["str"], "Say hello", greet);
#{ count: 0 }
}
pub fn greet(state, rx, args) {
state.count += 1;
rx.message(`hello ${args[0]} (${state.count})`);
}
Defined as pub fn at the top level; all optional except
init.
init(rx) -> Value
once at load; the return value is the plugin state
rx CtxValueunload(state, rx)
before the plugin is dropped (unload / hot reload)
state Valuerx CtxValueupdate(state, rx)
every frame, before rendering
state Valuerx CtxValueswitch_mode(state, rx)
on mode edges (query rx.mode()); fires once for the initial mode, and on command-mode entries
state Valuerx CtxValuecursor_moved(state, rx, x, y)
mouse motion, window logical coords (rx.session_coords(x, y) converts)
state Valuerx Ctxx f64y f64Valuemouse_input(state, rx, button, input)
button is "left"/"right"/"middle", input is "pressed"/"released"
state Valuerx Ctxbutton Stringinput StringValuecapture_mouse(state, rx, button, input) -> bool
button is "left"/"right"/"middle", input is "pressed"/"released"
state Valuerx Ctxbutton Stringinput StringboolReturns bool: true consumes the button event before ordinary
mouse hooks and builtin handling. Hooks run in reverse plugin load order.
Capture both press and release for a gesture started on the panel, including
releases outside it; let canvas-started gestures finish. This does not intercept
cursor motion, wheel events, or keyboard input. An invalid return value or error
disables the plugin.
view_added(state, rx, id)
view opened
state Valuerx Ctxid i64Valueview_removed(state, rx, id)
view closed
state Valuerx Ctxid i64Valueshade(state, rx, encoder)
per frame: record render/compute passes on the frame encoder (before screen composition)
state Valuerx Ctxencoder ScriptEncoderValuerender(state, rx, pass)
per frame: the live screen pass — everything drawn, present ahead — for screen-space pipeline drawing
state Valuerx Ctxpass ScriptPassValueoverlay(state, rx, pass)
per frame: draw above all ordinary render hooks, editor UI, and plugin previews; overlays run in plugin load order
state Valuerx Ctxpass ScriptPassValuedraw(state, rx)
per frame: UI-tier text/line/rectangle drawing (rx.draw_text / rx.draw_line are only live here)
state Valuerx CtxValuerx.mode() -> String
current mode ("normal", "visual", "command", … or a script mode)
Stringrx.prev_mode() -> Option<String>
previous mode
Option<String>rx.switch_mode(name) -> bool
builtin names switch builtin modes; any other name enters a script mode — builtin input handling is inert there, escape exits
name Stringboolrx.message(msg) -> ()
post to the message line
msg String()rx.set_fg(color) -> ()
picker semantics: old fg becomes bg; transparent ignored
color Rgba8()rx.offset() -> (f64, f64)
workspace pan offset
(f64, f64)rx.screen_size() -> (i64, i64)
the render stage target size; build screen orthos against it
(i64, i64)rx.cursor() -> (f64, f64)
cursor in session coordinates
(f64, f64)rx.session_coords(x, y) -> (f64, f64)
window logical → session
x f64y f64(f64, f64)rx.active_view_coords(x, y) -> (f64, f64)
session → active-view (floored)
x f64y f64(f64, f64)
Session, view, UI drawing coordinates, and pixel rows all increase downward
from a top-left origin. Mouse events use window logical coordinates; use
rx.session_coords(x, y) to apply the interface scale.
rx.active_view_id() -> i64
i64rx.views() -> Vec<ViewInfo>
snapshots, in view order
Vec<ViewInfo>rx.view_pixels(id, rect) -> Option<Bytes>
rgba8, row-major, from the recorded snapshot (see pitfalls); rect clamped
id i64rect RectOption<Bytes>rx.view_layer_pixels(id, layer, rect) -> Option<Bytes>
Read a recorded layer strip, with 0 as the bottom layer. The rectangle uses display-space y in 0..frame_height; x can span the frame strip.
id i64layer i64rect RectOption<Bytes>rx.layer_visibility(id) -> Vec<bool>
per-layer visible, bottom strip first; empty if the view doesn't exist
id i64Vec<bool>rx.layer_opacity(id) -> Vec<f64>
per-layer opacity (0.0–1.0), bottom strip first; empty if the view doesn't exist
id i64Vec<f64>rx.set_animation_frame(id, frame) -> bool
set the view's current animation frame; wraps by its frame count; false if the view doesn't exist
id i64frame i64boolrx.set_animation_sequence(id, frames) -> bool
set a custom playback sequence of zero-based frame indices; empty restores natural order; false for a missing view or an out-of-range index
id i64frames Vec<i64>boolrx.animation_sequence(id) -> Vec<i64>
the custom playback sequence; empty means natural order (and is also returned for a missing view)
id i64Vec<i64>rx.clear_animation_sequence(id) -> bool
restore natural frame-order playback without changing the visible frame; false if the view doesn't exist
id i64boolrx.set_animation_preview_visible(id, visible) -> bool
show or hide the built-in animation preview for the view; false if the view doesn't exist
id i64visible boolboolrx.touch_view(id) -> ()
mark modified → contents re-recorded (do this after painting a view via a pass)
id i64()rx.clear_view_rect(rect) -> ()
clear a rect of the active view to transparent — a recorded paint
rect Rect()rx.damage_view(id) -> ()
re-render from the snapshot, discarding unrecorded GPU-side paint (kill a preview)
id i64()
ViewInfo fields (read-only): id, width
(full sheet: frame_width * frames), height,
offset_x, offset_y, zoom,
frames, frame_width, frame_height,
animation_frame, animation_preview_visible (bool), nlayers (1 for a flat view),
active_layer (0 is the bottom strip).
rx.selection() -> Option<(i64, i64, i64, i64)>
normalized (x1 <= x2, y1 <= y2) regardless of drag direction
Option<(i64, i64, i64, i64)>rx.set_selection(x1, y1, x2, y2) -> ()
x1 i64y1 i64x2 i64y2 i64()rx.clear_selection() -> ()
()The selection is cleared when the session returns to normal mode.
rx.setting(name) -> Value
bool / int / float / string / tuple; unit if absent
name StringValuerx.set_setting(name, value) -> bool
Set an existing scalar setting; the value must match its current type.
name Stringvalue Valueboolrx.declare_setting(name, default) -> bool
plugin-owned, :set-able like builtins; re-declaring is a no-op
name Stringdefault Valueboolrx.register_command(name, sig, help, handler) -> bool
sig is typed params: "int", "float", "str", "color", "bool", suffix ? for optional; handler is handler(state, rx, args)
name Stringsig Vec<String>help Stringhandler Functionboolrx.register_command_repeating(name, sig, help, handler) -> bool
same, repeats while the bound key is held
name Stringsig Vec<String>help Stringhandler Functionboolrx.bind(mode, mapping) -> bool
script-tier binding in a script mode; :map syntax, e.g. "<tab> :v/prev", "'r' :rotate 90 {:rotate 0}"; wins over general bindings while the mode is active, may fire with the mouse held
mode Stringmapping Stringboolrx.run_builtin(invocation) -> bool
run a builtin command (script commands aren't resolvable through it)
invocation Stringbool
Character bindings ('r') fire on char/received, not
raw key input. The command line is not reachable from script modes —
drive script modes entirely through bindings.
rx.export(name, handler) -> bool
offer handler(state, rx, args) to other plugins, run with the exporting plugin's state
name Stringhandler Functionboolrx.call_plugin(plugin, name, args) -> Result<Value, String>
call another plugin's export
plugin Stringname Stringargs Vec<Value>Result<Value, String>draw hook only)rx.draw_text(text, x, y, color) -> ()
UI coordinates; no-op outside draw
text Stringx f64y f64color Rgba8()rx.draw_line(p1, p2, options) -> Result<(), String>
Screen-space line with explicit width and end caps; draw hook only.
p1, p2options.coloroptions.widthoptions.capResult<(), String>rx.draw_line((10.0, 10.0), (80.0, 30.0), #{
color: rx::rgb(255, 51, 102), width: 3.0, cap: "round",
}).unwrap();
Breaking change: replace the old third color argument with #{ color } and handle the result.
rx.draw_rect(rect, options) -> Result<(), String>
Rectangle with optional fill and a joined stroke; draw hook only.
rectoptions.filloptions.strokestroke.alignstroke.joinResult<(), String>rx.draw_rect(rx::rect(10.0, 10.0, 80.0, 60.0), #{
stroke: #{ color: rx::rgb(255, 51, 102), width: 2.0, align: "outside" },
}).unwrap();
rx.read_file(path) -> Option<String>
relative to the plugin's directory (shaders etc.)
path StringOption<String>rx.read_png(path) -> Option<(i64, i64, Bytes)>
decode an 8-bit RGBA PNG relative to the plugin's directory (cursor/icon assets) → (w, h, pixels), row-major rgba8 — the layout upload/write_png use
path StringOption<(i64, i64, Bytes)>rx.write_png(path, w, h, data) -> bool
rgba8 row-major → PNG; path resolves like :export (cwd-relative, not plugin-relative); loud on both outcomes
path Stringw i64h i64data Bytesbool
All constructors return Option and post the error (shader
compile errors included) to the message line on None.
rx.create_texture(w, h) -> Option<ScriptTexture>
rgba8 (sRGB), max dimension 8192; owned by the plugin, dropped with its state
w i64h i64Option<ScriptTexture>rx.texture_pixels(tex) -> Option<Bytes>
readback, mirror of upload; forces a GPU sync — command-handler tool, not per-frame (see pitfalls)
tex ScriptTextureOption<Bytes>rx.create_shader(wgsl) -> Option<ScriptShader>
wgsl StringOption<ScriptShader>rx.create_render_pipeline(shader, options) -> Option<ScriptPipeline>
Create a pipeline with explicit blending and independent topology and vertex layout.
shader ScriptShaderoptions Objectoptions.vertex Stringoptions.fragment Stringoptions.blend String | Object"alpha" composites straight-alpha source colors; "premultiplied_alpha" composites already-premultiplied colors; "replace" overwrites exact RGBA, including zero alpha. An object instead supplies separate color and alpha equations, documented below.options.textures i640. Number of texture + nearest-sampler groups, 0–3. Group 0 always uses the transform/params layout; texture groups occupy slots 1 through this count.options.topology String"triangle_list". Accepted: "triangle_list", "triangle_strip", "point_list", "line_list", "line_strip". Independent of the vertex layout.options.vertex_layout String"sprite": position (Float32x3), UV (Float32x2), color (Unorm8x4), opacity (Float32), at locations 0–3, with a 28-byte vertex stride. "none" declares no vertex buffers; the shader can generate geometry from @builtin(vertex_index).options.format String"rgba8unorm-srgb". Also accepts "rgba8unorm". Must match the render attachment: use the default for editor targets and texture.view(), or linear format for texture.raw_view().options.blend.color Objectsrc, dst, and op fields as below.options.blend.alpha Objectsrc / dst String"zero", "one", "src", "one_minus_src", "src_alpha", "one_minus_src_alpha", "dst", "one_minus_dst", "dst_alpha", "one_minus_dst_alpha", "src_alpha_saturated". Source is the fragment output; destination is the stored pixel. wgpu validates supported combinations.op String"add", "subtract", "reverse_subtract", "min", "max". Add/subtract combine the factored source and destination; min/max choose the minimum/maximum of the source and destination components, ignoring factors.Option<ScriptPipeline>The pipeline has one color attachment, all RGBA channels writable, no depth attachment, and no multisampling. Arbitrary vertex and binding layouts are not exposed.
let pipeline = rx.create_render_pipeline(shader, #{
vertex: "vs_main", fragment: "fs_main", textures: 1,
blend: "replace",
}).unwrap();
A vertex-index-driven point pipeline uses topology: "point_list", vertex_layout: "none" in this constructor.
rx.create_compute_pipeline(shader, entry, inputs) -> Option<ScriptComputePipeline>
inputs = 1–4 input textures at bindings 0..n, write-only rgba8 storage output at binding n (one group)
shader ScriptShaderentry Stringinputs i64Option<ScriptComputePipeline>texture is a ScriptTexture returned by rx.create_texture.
texture.view() -> ScriptTextureView
Create a persistent rgba8unorm-srgb view over a script-owned texture.
ScriptTextureViewformat: "rgba8unorm-srgb".texture.raw_view() -> ScriptTextureView
Create a persistent rgba8unorm view over a script-owned texture.
ScriptTextureViewformat: "rgba8unorm".texture.width() -> i64
Read the texture width.
i64texture.height() -> i64
Read the texture height.
i64texture.upload(bytes) -> bool
Replace the entire texture with pixel data.
bytes Bytesbooltexture.fill(color) -> ()
Fill the entire texture with one color.
color Rgba8()
Render/point pipeline layout: group 0 = transform + params, groups
1..=textures = one texture + nearest sampler each. Texture
bindings are visible to both vertex and fragment stages.
rx.create_transform_bind_group(w, h, transform) -> Option<ScriptBindGroup>
group 0: ortho for a w×h target composed with transform; params slot zeroed (WGSL that doesn't declare binding 1 is unaffected)
w i64h i64transform Mat4Option<ScriptBindGroup>rx.create_transform_params_bind_group(w, h, transform, params) -> Option<ScriptBindGroup>
adds user params (1–64 floats) at group 0 binding 1: var<uniform> params: array<vec4<f32>, N> where N = ceil(len/4) — the buffer is sized to the floats passed, not a fixed 64, so declare N to match or it's a validation error; packed in order, zero-padded to the vec4; an f32 holds 24 exact integer bits — pass 32-bit masks as two 16-bit halves
w i64h i64transform Mat4params Vec<f64>Option<ScriptBindGroup>rx.create_texture_bind_group(view) -> Option<ScriptBindGroup>
Bind a texture view and nearest sampler using the shared texture-group layout.
view ScriptTextureViewOption<ScriptBindGroup>encoder.view_bind_group(view_id) -> Result<ScriptBindGroup, String>
shade-stage: the view's live layer texture as input — unrecorded paints included, unlike view_pixels; built per call, so resizes are tracked next frame; binding a view as input to a pass targeting it is a validation error (disables the plugin)
view_id i64Result<ScriptBindGroup, String>rx.create_compute_bind_group(inputs, output) -> Option<ScriptBindGroup>
1–4 input textures + storage output; must match the pipeline's declared count; compute reads raw (no sRGB decode)
inputs Vec<ScriptTexture>output ScriptTextureOption<ScriptBindGroup>rx.create_sprite_vertices(texture, options) -> Option<ScriptBuffer>
convenience helper for the existing render-pass API: creates a six-vertex textured quad; bind the buffer with pass.set_vertex_buffer and draw with pass.draw
texture ScriptTextureoptions Object#{ dst, src, color, opacity }. Only dst is required. Unknown fields and incorrectly typed values are rejected. The descriptor can be reused across calls.options.dst Rectoptions.src Rectoptions.color Rgba8options.opacity f64Option<ScriptBuffer>buffer is a ScriptBuffer returned by rx.create_sprite_vertices.
buffer.count() -> i64
Read the number of vertices in a buffer.
i64shade encoder)
shade(state, rx, encoder) records passes on the frame's command
encoder. One pass is open at a time: beginning a new pass auto-ends its
predecessors, and any pass left open is ended when the hook returns.
load is "load" (keep) or "clear" (to
transparent).
encoder.view_layer(view_id) -> Result<ScriptTextureView, String>
Access the live artwork texture view.
view_id i64Result<ScriptTextureView, String>encoder.view_staging(view_id) -> Result<ScriptTextureView, String>
Access the live preview-overlay texture view.
view_id i64Result<ScriptTextureView, String>encoder.begin_render_pass(target, options) -> Result<ScriptPass, String>
Render into any texture view; beginning a pass ends the previous open pass.
target ScriptTextureViewoptions Objectoptions.load String"load" preserves existing pixels; "clear" clears the entire attachment before drawing. Output is always stored.options.label String"script_pass".options.clear Vec<f64>load: "clear" only. Exactly four finite linear RGBA floats in 0–1. Default [0.0, 0.0, 0.0, 0.0] (transparent black). Supplying this with load: "load" is an error.Result<ScriptPass, String>encoder.begin_view_pass(label, view_id, load) -> Result<ScriptPass, String>
a view's layer — follow with rx.touch_view(id) so the edit is recorded (and undoable)
label Stringview_id i64load StringResult<ScriptPass, String>encoder.begin_staging_pass(label, view_id, load) -> Result<ScriptPass, String>
the view's staging overlay: composited above the view, cleared every frame — for uncommitted previews
label Stringview_id i64load StringResult<ScriptPass, String>encoder.begin_compute_pass(label) -> Result<ScriptComputePass, String>
label StringResult<ScriptComputePass, String>pass is a ScriptPass returned by encoder.begin_render_pass, encoder.begin_view_pass, or encoder.begin_staging_pass, or received by the render or overlay hook. Use it during the hook that created or received it. Beginning another encoder pass or returning from the hook ends it.
pass.draw_sprite(source, options) -> Result<(), String>
draw a textured quad with the built-in shader, pipeline, bindings, and geometry
source ScriptTexture | ScriptTextureViewoptions.dst Rectoptions.src Rectoptions.transform Mat4options.color Rgba8options.opacity f64options.blend String"alpha" (default) or "replace". Replacement writes transparent pixels too.Result<(), String>The pass supplies target dimensions and format. Sampling is nearest-neighbor, clamp-to-edge; no CPU readback occurs. Raw source/target views bypass the corresponding sRGB conversion. Artwork sources contain all frame and layer strips; crop and compose them explicitly. Do not sample the target's own texture subresource.
This changes the active pipeline, bind groups 0–1, and vertex-buffer slot 0. Rebind these before custom drawing in the same pass. Viewport and scissor state are preserved. For examples, see the scripting guide.
pass.draw_sprite(state.icon, #{
dst: rx::rect(10.0, 10.0, 26.0, 26.0),
opacity: 0.9,
}).unwrap();
pass.set_pipeline(p) -> Result<(), String>
Select the pipeline.
p ScriptPipelineResult<(), String>pass.set_bind_group(i, g) -> Result<(), String>
Bind resources for the pipeline.
i i64g ScriptBindGroupResult<(), String>pass.end() -> ()
End the pass; calling end again is harmless.
()pass.set_vertex_buffer(slot, b) -> Result<(), String>
Bind a vertex buffer.
slot i64b ScriptBufferResult<(), String>pass.draw(vertices, instances) -> Result<(), String>
Record a draw call.
vertices i64instances i64Result<(), String>compute_pass is a ScriptComputePass returned by encoder.begin_compute_pass. Use it during the hook that created it. Beginning another encoder pass or returning from the hook ends it.
compute_pass.set_pipeline(p) -> Result<(), String>
Select the pipeline.
p ScriptComputePipelineResult<(), String>compute_pass.set_bind_group(i, g) -> Result<(), String>
Bind resources for the pipeline.
i i64g ScriptBindGroupResult<(), String>compute_pass.end() -> ()
End the pass; calling end again is harmless.
()compute_pass.dispatch(x, y, z) -> Result<(), String>
Record a compute dispatch.
x i64y i64z i64Result<(), String>
The render(state, rx, pass) hook receives a
ScriptPass over the screen: same methods, screen-sized target
(rx.screen_size()), re-begun per hook so errors attribute to the
plugin that recorded them.
rx::)rx::rgb(r, g, b) -> Rgba8
alpha 255; fields .r .g .b .a readable
r i64g i64b i64Rgba8rx::rgba(r, g, b, a) -> Rgba8
explicit alpha; blends in draw_line/draw_text (e.g. translucent connection wires)
r i64g i64b i64a i64Rgba8rx::rect(x1, y1, x2, y2) -> Rect
fields .x1 .y1 .x2 .y2
x1 f64y1 f64x2 f64y2 f64Rectrx::mat4_identity() -> Mat4
Mat4rx::mat4_translation(x, y) -> Mat4
x f64y f64Mat4rx::mat4_scale(sx, sy) -> Mat4
sx f64sy f64Mat4rx::mat4_rotation_z(theta) -> Mat4
theta f64Mat4rx::mat4_transform_point(m, x, y) -> (f64, f64)
m Mat4x f64y f64(f64, f64)rx::atan2(y, x) -> f64
radians
y f64x f64f64view_pixels, selection rects, and quad src and
dst rects all use y-down coordinates. Painting pixel y=10
lands in byte row 10. A cropped buffer starts at the clamped rectangle's
top-left corner. Normalize reversed selection corners before drawing.view_pixels reads the
recorded snapshot: it sees a mutation only after the view was
touched and the frame recorded — split mutate and read into separate
commands a few frames apart. encoder.view_bind_group is the
live read: it sees same-frame, unrecorded paint.texture_pixels is synchronous and sees only
submitted work: a readback in the same frame as a shade/render mutation
reads stale pixels. Same split applies.blend: "replace" and load the existing target to overwrite exact RGBA pixels,
or rx.clear_view_rect(rect) for an active-view rectangle. No CPU copy is needed.rx.