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
14,626 changes: 11,434 additions & 3,192 deletions Cargo.lock

Large diffs are not rendered by default.

40 changes: 31 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exclude = ["examples/consumer"]
members = [
"crates/frame",
"crates/frame-architect",
"crates/frame-view",
"crates/frame/tools/cli",
"crates/frame/tools/desktop",
"crates/frame/tools/library-server",
Expand All @@ -22,7 +23,7 @@ members = [
default-members = [
"crates/frame",
"crates/frame-architect",
"crates/frame-architect",
"crates/frame-view",
"crates/frame/tools/cli",
"crates/frame/tools/desktop",
"crates/frame/tools/library-server",
Expand All @@ -38,19 +39,24 @@ authors = ["FastTrack Studios"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/FastTrackStudios/frame"

# architect v0.10.0's pins (its Cargo.toml and lock): vox rc.7, facet rc.7,
# vello 0.11, wgpu 30, anyrender 0.14 / anyrender_vello 0.15, Blitz and
# nice-plug at fts-upstream-2026-11. A Frame consumer repeats the [patch]
# tables below.
# architect's pins (its Cargo.toml and lock): vox rc.7, facet rc.7, vello
# 0.11, wgpu 30, anyrender 0.14 / anyrender_vello 0.15, Dioxus at 430cfb5,
# Blitz and nice-plug at fts-upstream-2026-11. architect is at main past
# v0.10.0 (cbdc7c7, #258) for its view kinds (#250), which `frame-view`
# provides one of; back to a tag at architect's next release. A Frame
# consumer repeats the [patch] tables below.
[workspace.dependencies]
anyrender = "0.14"
anyrender_vello = "0.15"
anyrender_svg = "0.15"
architect = { git = "https://github.com/FastTrackStudios/architect", tag = "v0.10.0" }
architect = { git = "https://github.com/FastTrackStudios/architect", rev = "cbdc7c7d7149d40577688554c8b1ad652466f1dc" }
architect-extensions = { git = "https://github.com/FastTrackStudios/architect", rev = "cbdc7c7d7149d40577688554c8b1ad652466f1dc" }
architect-workspace = { git = "https://github.com/FastTrackStudios/architect", rev = "cbdc7c7d7149d40577688554c8b1ad652466f1dc" }
base64 = "0.22.1"
blake3 = { version = "1.8" }
chrono = { version = "0.4", default-features = false, features = ["clock", "std", "wasmbind"] }
crdt = { git = "https://github.com/FastTrackStudios/architect", tag = "v0.10.0" }
crdt = { git = "https://github.com/FastTrackStudios/architect", rev = "cbdc7c7d7149d40577688554c8b1ad652466f1dc" }
dioxus = { git = "https://github.com/DioxusLabs/dioxus", rev = "430cfb563f1b70602ef387f0c80f171d84527b41", default-features = false, features = ["lib"] }
facet = { version = "0.50.0-rc.7", features = ["reflect", "chrono", "uuid"] }
facet-json = { version = "0.50.0-rc.7" }
facet-styx = { version = "5.0.0-rc.7" }
Expand Down Expand Up @@ -83,11 +89,19 @@ vello = "0.11"
wgpu = "30"
zip = { version = "2", default-features = false }

# architect v0.10.0's patch tables (FastTrackStudios/vendor at v0.7.1 where
# architect has v0.7.0), the entries that reach Frame's graph.
# architect's patch tables, repeated: architect's workspace-hack (#258)
# reaches its Blitz and Dioxus graph.
[patch.crates-io]
blitz-dom = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
blitz-traits = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
blitz-paint = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
blitz-shell = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
blitz-net = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
blitz-html = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
anyrender = { git = "https://github.com/FastTrackStudios/blitz", tag = "fts-upstream-2026-11" }
anyrender_vello = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
anyrender_vello_hybrid = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
winit-uikit = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
wgpu-hal = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
raw-window-metal = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
styx-format = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.1.0" }
Expand All @@ -96,6 +110,14 @@ phon-jit = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.2.3"
facet-core = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.4.0" }
vox-phon = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.3.0" }

# architect's Dioxus patch (FastTrackStudios/vendor, UPSTREAM.md), which
# reaches Frame's graph through architect's workspace-hack and
# `frame-view`'s guest.
[patch."https://github.com/DioxusLabs/dioxus"]
dioxus-native = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
dioxus-native-dom = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }
dioxus-vdom = { git = "https://github.com/FastTrackStudios/vendor", tag = "v0.7.1" }

[workspace.lints.rust]
# Flipped from "allow" 2026-07-28 (task cleanup): the blanket allows are
# WHY ~30k lines of dead code accumulated silently. The task tree is
Expand Down
26 changes: 26 additions & 0 deletions crates/frame-view/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "frame-view"
description = "The frame.surface view kind for architect's embedded views: its registration, a Frame surface painted as SVG into a Dioxus view, and `frame-surface`, a process extension showing one"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true

[lints]
workspace = true

[dependencies]
frame = { path = "../frame", default-features = false, features = ["embed"] }
# The kinds registry (the model, no Dioxus).
architect-workspace = { workspace = true }
# A process extension's end: its view's VirtualDom, its actions.
architect-extensions = { workspace = true, features = ["guest"] }
dioxus = { workspace = true }
kurbo = { workspace = true }
peniko = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

[[bin]]
name = "frame-surface"
path = "src/bin/frame-surface.rs"
51 changes: 51 additions & 0 deletions crates/frame-view/extension.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# `frame-surface`, a process extension (architect's extensions, kind
# `process`): its one view is of the `frame.surface` kind, a Frame
# document's surface drawn by this process (the built-in knob and meter,
# surfaces/knob-meter.frame, until a document is loaded); its actions load
# the document and move and read its params and feeds, so an app's Action
# List, its MCP server and its CLI drive it. `frame-surface --manifest`
# prints this file, for a host that runs the program as a development
# copy.
id = "frame.surface"
name = "Frame surface"
version = "0.2.0"
kind = "process"
api = "0.1"
description = "A Frame document's surface, drawn by Frame in a process of its own: its params and feeds moved by actions, a save to the document reloading it."
authors = ["FastTrackStudios"]

[entry]
path = "frame-surface"

[[actions]]
id = "FRAME_SURFACE_LOAD"
name = "Frame surface: load a document"
description = "Shows the Frame document at `path` (a .frame file) and watches it: a save reloads it, its params and feeds kept where they were. Its params are its param-bound controls (0 to 1), its feeds its feed-bound ones (dB)."
effect = "mutating"
params = '{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}'

[[actions]]
id = "FRAME_SURFACE_PARAM_SET"
name = "Frame surface: set a param"
description = "Moves the surface's param `addr` (its control's binding, e.g. surface/drive) to `value`, 0 to 1."
effect = "mutating"
params = '{"type":"object","properties":{"addr":{"type":"string"},"value":{"type":"number","minimum":0,"maximum":1}},"required":["addr","value"]}'

[[actions]]
id = "FRAME_SURFACE_FEED"
name = "Frame surface: push a feed reading"
description = "A reading of the surface's feed `id` (e.g. level): `values`, dB, one per channel (-60 is an empty meter, +6 a full one)."
effect = "mutating"
params = '{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"array","items":{"type":"number"}}},"required":["id","values"]}'

[[actions]]
id = "FRAME_SURFACE_STATE"
name = "Frame surface: state"
description = "What the surface shows: `document` (its file, null for the built-in one), `loads`, `error` (why the last save did not take), `params` ({addr, name, value 0..1}) and `feeds` ({id, name, values dB})."
effect = "read_only"

[[views]]
id = "surface"
title = "Frame surface"
icon = "gauge"
kind = "frame.surface"
80 changes: 80 additions & 0 deletions crates/frame-view/src/bin/frame-surface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! `frame-surface`: a process extension (`extension.toml`) whose view, of
//! the `frame.surface` kind, is a Frame document's surface, and whose
//! actions load the document and move and read its params and feeds
//! (`frame_view::shown`). Started by a host; run by hand it says so and
//! exits. `frame-surface --manifest` prints its manifest, for a host that
//! runs it as a development copy.
//!
//! It shows the document at `FRAME_SURFACE_DOCUMENT` when that is set,
//! the built-in knob and meter otherwise, until `FRAME_SURFACE_LOAD`
//! loads one; a loaded document is watched, and a save reloads it.

use std::path::Path;

use architect_extensions::guest::{Guest, GuestError};
use frame_view::shown;
use frame_view::view::FrameSurface;
use serde_json::Value;

fn text<'a>(args: &'a Value, key: &str) -> Result<&'a str, String> {
args.get(key).and_then(Value::as_str).ok_or_else(|| {
format!(
"`{key}` is a string, not {}",
args.get(key).unwrap_or(&Value::Null)
)
})
}

fn main() -> Result<(), GuestError> {
if std::env::args().nth(1).as_deref() == Some("--manifest") {
print!("{}", frame_view::MANIFEST);
return Ok(());
}
if let Some(path) = std::env::var_os("FRAME_SURFACE_DOCUMENT")
&& let Err(e) = shown::load(Path::new(&path))
{
eprintln!("frame-surface: {e}");
}
shown::watch();
Guest::new()
.view("surface", FrameSurface)
.action("FRAME_SURFACE_LOAD", |args: Value| async move {
shown::load(Path::new(text(&args, "path")?))
})
.action("FRAME_SURFACE_PARAM_SET", |args: Value| async move {
let addr = text(&args, "addr")?;
let value = args
.get("value")
.and_then(Value::as_f64)
.filter(|v| (0.0..=1.0).contains(v))
.ok_or_else(|| {
format!(
"`value` is a number from 0 to 1, not {}",
args.get("value").unwrap_or(&Value::Null)
)
})?;
shown::set_param(addr, value)
})
.action("FRAME_SURFACE_FEED", |args: Value| async move {
let id = text(&args, "id")?;
let values: Vec<f64> = match args.get("values") {
Some(Value::Array(vs)) => vs
.iter()
.map(|v| {
v.as_f64()
.ok_or_else(|| format!("a reading is dB, not {v}"))
})
.collect::<Result<_, _>>()?,
Some(Value::Number(n)) => n.as_f64().into_iter().collect(),
v => {
return Err(format!(
"`values` is dB, a number per channel, not {}",
v.unwrap_or(&Value::Null)
));
}
};
shown::feed(id, &values)
})
.action("FRAME_SURFACE_STATE", |_| async { Ok(shown::state()) })
.serve()
}
57 changes: 57 additions & 0 deletions crates/frame-view/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! `frame.surface`, a view kind for architect's embedded views
//! (architect's `docs/content/architecture/embedded-views.md`, #224): a
//! Frame document's controls, painted by Frame, in an architect pane.
//!
//! architect defines the kinds contract and never depends on a provider;
//! this crate is the provider (architect#220's layering). An app that
//! links it declares the kind with [`register`]; an app that does not
//! (architect's lab) runs [`frame-surface`](../frame_surface/index.html),
//! the process extension whose view is of this kind ([`MANIFEST`]), and
//! declares [`spec`]'s row itself.
//!
//! The surface is a Frame document's ([`surface`]): loaded from a `.frame`
//! file and reloaded when it is saved ([`shown`]), its params and feeds
//! the ones its controls bind, moved by a host's actions and by its own
//! knobs. It is painted through Frame's [`Canvas`](frame::embed::Canvas)
//! seam into SVG ([`svg`]) and shown as a Dioxus view ([`view`]), so it
//! draws wherever a Dioxus view does, a remote one included. The GPU kind
//! (a Blitz paint source, Frame's own vello scene) replaces the painter,
//! not the kind.

pub mod shown;
pub mod surface;
pub mod svg;
pub mod view;

use architect_workspace::{Capability, KindError, KindSpec, Placement, ViewHost, ViewKinds};

/// The kind's id, what an extension's `[[views]] kind` names.
pub const KIND: &str = "frame.surface";

/// Who provides it, as the kinds registry shows.
pub const PROVIDER: &str = "frame-view";

/// `frame-surface`'s manifest: its view (of [`KIND`]) and its actions.
pub const MANIFEST: &str = include_str!("../extension.toml");

/// The kind as this crate provides it: in a desk app's window (Blitz, a
/// remote view included), as a pane or inline; it takes pointer input and
/// its controls run actions.
#[must_use]
pub fn spec() -> KindSpec {
KindSpec::new(KIND, PROVIDER)
.host(ViewHost::Blitz)
.placement(Placement::Pane)
.placement(Placement::Embed)
.capability(Capability::Input)
.capability(Capability::Actions)
}

/// Declares [`KIND`] in `kinds`.
///
/// # Errors
///
/// Another provider registered it first.
pub fn register(kinds: &mut ViewKinds) -> Result<(), KindError> {
kinds.register(spec())
}
Loading