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
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ bytes = "1"
humantime = "2.4"
humantime-serde = "1.1.1"
pollster = "1.0"
# The effects the studio UI draws inside egui. Rune is pre-release and asks for
# a rev pin; its `dev` branch is never rewritten.
rune-fx = { git = "https://github.com/anomalyco/rune", rev = "b99c7b04e75c0b7587fe8d09ba6a679e11ede9c9" }
serde = { version = "1", features = ["derive"] }
serde_with = "3"
tempfile = "3"
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ Every panel is a tab you can drag, split, or stack.
| Replay | Replay the last moments of the program or a source in a replay source, slowed down if you like, or save them as a clip. |
| Hotkeys | Bind keys to cut, transition, preview a scene, replay, save a clip, and start or stop recording, and clear them (see [Hotkeys](#hotkeys)). |

While recording, a red dot in the Program panel says so.

A new source, from the Sources or Devices panel, is placed over whatever the
Items panel shows. The **top layer** is drawn over program, and over the
preview of a scene of the program's size, and stays on through a cut or
Expand Down Expand Up @@ -971,14 +973,33 @@ with, such as a function key, a keypad key, or one with modifiers:
bound. Without the portal, or if the dialog is declined, bound keys work only
while the studio has focus, and the panel says so.

The bindings, the snap and grid settings, and the output **Listen** plays on
are kept per user in the app's settings, `~/Library/Application
The bindings, the snap and grid settings, the [effects](#effects) setting, and
the output **Listen** plays on are kept per user in the app's settings, `~/Library/Application
Support/moq-studio` on macOS, `$XDG_DATA_HOME/moq-studio` (or
`~/.local/share/moq-studio`) on Linux, and `%APPDATA%\moq-studio\data` on
Windows, apart from the scene file, so a scene file moves between machines
without one operator's keys. A key bound to a scene follows it when renamed in
the studio.

## Effects

The window draws effects of its own, which never reach the program, a
published scene, or a recording:

- The Program monitor glows red while the studio is connected to a relay,
the recording dot pulses, and each cut or transition, from its button or a
hotkey, bursts on the monitor.
- The **Cut** button is polished metal that ripples under the pointer, the
Hotkeys panel shows each bound key on a keycap, each panel has a faint grain,
and first launch draws its screen over a soft gradient.
- The Health panel and its tab pulse amber while the worst problem is
degraded, and flicker with electricity while it is unhealthy.

**View > Effects** sets how they move: **Full**, **Reduced**, which holds each
on one still pose, or **Off**. Until you pick one it follows the system's
reduced-motion setting: Reduce motion on macOS, animation effects on Windows,
and on Linux the desktop's, through its Settings portal.

## Replay

Add a `replay` source and place it like any other, such as full screen in a
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ than opening an m2 for them.

- [Viewer-side composition](/quest/m1/compose/README.md) - sources as their own tracks plus a layout track, composed by viewers and by a server
- [Scene graph view](/quest/m1/scene-graph.md) - a node view of picture and audio scenes, their routing, volume, pan, and waveforms
- [Effects in the studio UI](/quest/m1/ui-effects.md) - rune-fx glow, bursts, metal and gradient chrome, and alert effects in the egui UI
- [Rune UI spike](/quest/m1/rune-spike.md) - the monitors prototyped on Rune, with a verdict on moving the UI off egui
7 changes: 2 additions & 5 deletions quest/m1/rune-spike.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ moving or staying.
## Plan

Decided 2026-11-07 with the maintainer (/quest-plan), after choosing
[UI effects](/quest/m1/ui-effects.md) on rune-fx inside egui first:
[UI effects](https://github.com/moq-dev/studio/pull/85) on rune-fx inside
egui first, whose experience feeds the verdict:

- ✅ **Prototype the Preview and Program monitors:** live compositor textures,
drag-to-edit items, and the Cut button. They are the UI's hardest part, so
Expand All @@ -23,7 +24,3 @@ Decided 2026-11-07 with the maintainer (/quest-plan), after choosing
report.
- The prototype stays off `main`: the PR deletes this quest and links the
prototype branch. A move to Rune, if picked, is planned with /quest-plan.

## Required

- [Effects in the studio UI](/quest/m1/ui-effects.md) - rune-fx inside egui first, whose experience feeds the verdict
50 changes: 0 additions & 50 deletions quest/m1/ui-effects.md

This file was deleted.

19 changes: 14 additions & 5 deletions rs/moq-studio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ moq-net = { workspace = true }
# The guest room: its path convention, roster, and token claims.
moq-room = { workspace = true, features = ["async"] }
moq-scene = { workspace = true }
moq-stats = { workspace = true }
moq-stats = { workspace = true, features = ["async"] }
moq-tokio = { workspace = true, default-features = false, features = ["net"] }
moq-video = { workspace = true }
pollster = { workspace = true }
Expand All @@ -74,6 +74,8 @@ qrcodegen = "1.8"
same-file = "1.0.6"
# The guests removed from scenes, kept beside the scene file.
serde = { workspace = true }
# Glows, pulses, bursts, and fills in the UI, drawn on its wgpu device.
rune-fx = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["full"] }
toml = { workspace = true }
Expand All @@ -87,9 +89,10 @@ usage-rs = { workspace = true, features = ["completions", "config"] }
winit = { version = "0.30", default-features = false, features = ["serde"] }

[target.'cfg(target_os = "linux")'.dependencies]
# The GlobalShortcuts portal for hotkeys on Wayland. 0.13, as moq-video's
# PipeWire capture uses, so the two share one D-Bus stack.
ashpd = { version = "0.13", default-features = false, features = ["global_shortcuts", "tokio"] }
# The GlobalShortcuts portal for hotkeys on Wayland, and the Settings portal's
# reduced motion for the effects. 0.13, as moq-video's PipeWire capture uses,
# so the two share one D-Bus stack.
ashpd = { version = "0.13", default-features = false, features = ["global_shortcuts", "settings", "tokio"] }
futures-util = "0.3"
# The virtual camera's v4l2loopback fallback. Pure Rust with its kernel bindings
# checked in, and already in the graph through moq-video's capture.
Expand All @@ -108,9 +111,15 @@ x11rb = { version = "0.13", features = ["xkb"] }
# goes through the GlobalShortcuts portal.
global-hotkey = "0.9"

[target.'cfg(target_os = "macos")'.dependencies]
# Whether the system asks apps to reduce motion, for the effects. eframe's
# version, already in the graph through winit.
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSAccessibility", "NSWorkspace"] }

[target.'cfg(windows)'.dependencies]
# Which virtual key the foreground window's layout puts on each key, so a
# hotkey registers the key pressed.
# hotkey registers the key pressed, and whether the system asks apps to
# reduce motion, for the effects.
windows-sys = { version = "0.61", features = ["Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging"] }

[dev-dependencies]
Expand Down
15 changes: 11 additions & 4 deletions rs/moq-studio/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ pub struct State {
/// How many times the collection was reloaded from its files, so the UI
/// can let go of what a reload may have removed.
pub reloads: u64,
/// How many times the preview was taken to program, by a cut or a
/// transition, so the UI can mark each however it was fired.
pub takes: u64,
pub sources: Sources,
pub compositor: Compositor,
/// Each virtual camera on: the program's under `None`, and each published
Expand Down Expand Up @@ -302,15 +305,18 @@ impl State {

/// Put the preview on program at once; see [`Compositor::cut`].
pub fn cut(&mut self) -> anyhow::Result<()> {
Ok(self.compositor.cut(&self.collection, &self.sources)?)
self.compositor.cut(&self.collection, &self.sources)?;
self.takes += 1;
Ok(())
}

/// Take the preview to program by `transition`, starting at `now`; see
/// [`Compositor::transition`].
pub fn transition(&mut self, transition: moq_scene::Transition, now: Instant) -> anyhow::Result<()> {
Ok(self
.compositor
.transition(&self.collection, transition, &self.sources, now)?)
self.compositor
.transition(&self.collection, transition, &self.sources, now)?;
self.takes += 1;
Ok(())
}

/// Restart the source called `name`, as an edit would, such as to reload a
Expand Down Expand Up @@ -905,6 +911,7 @@ impl Engine {
history,
dropped,
reloads: 0,
takes: 0,
sources,
compositor,
cameras: BTreeMap::new(),
Expand Down
Loading