diff --git a/crates/oakcommon/src/handle.rs b/crates/oakcommon/src/handle.rs
deleted file mode 100644
index 405d79cda..000000000
--- a/crates/oakcommon/src/handle.rs
+++ /dev/null
@@ -1,402 +0,0 @@
-// Oak Video Editor - Non-Linear Video Editor
-// Copyright (C) 2026 Oak Team
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-//! Refcounted-handle scaffolding. Same pattern as the oakplugin crate
-//! (`src/plugin/rust/src/handle.rs`); intentionally duplicated rather
-//! than shared — each module DLL must run its own addref/release code
-//! (the function pointers in a handle always point into the DLL that
-//! created the object).
-//!
-//! Mirrors the C side (`include/common/handle.h`):
-//!
-//! ```c
-//! typedef struct OakXxx {
-//! void *ctx;
-//! void (*addref)(void *ctx);
-//! void (*release)(void *ctx);
-//! uint32_t abi_version;
-//! } OakXxx;
-//! ```
-//!
-//! Handles are passed by value; `ctx` points to a heap [`RefBox`].
-//! The `addref`/`release` function pointers always point into this crate.
-
-use std::panic::{catch_unwind, AssertUnwindSafe};
-use std::sync::atomic::{AtomicU32, Ordering};
-
-/// ABI version stamped into every handle.
-pub const OAKCOMMON_ABI_VERSION: u32 = 1;
-
-/// Heap box behind a handle's `ctx`.
-///
-/// `value` is deliberately the FIRST field so that `ctx` (which points at
-/// the box head) aliases the boxed value: the C-facing `ffi.rs` code
-/// frequently casts `ctx` directly to `*mut T` / `*const T`, and that cast
-/// only lands on the real value when it sits at offset 0. `refs` follows,
-/// located via field access (never a raw offset) by the addref/release
-/// thunks. `#[repr(C)]` locks the layout so those direct casts are sound.
-// CPP-PARITY: matches `include/common/handle.h` where `ctx` is an opaque
-// pointer; the C side never dereferences it, so this layout is private to
-// this crate.
-#[repr(C)]
-pub struct RefBox {
- /// Boxed value (at offset 0 — see module doc).
- pub value: T,
- /// Atomic reference count.
- pub refs: AtomicU32,
-}
-
-/// The shared ABI value-handle type (single-lib unification, see
-/// `docs/zh/plans/riir/single-lib.md`): one canonical
-/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
-/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
-/// source-compatible.
-pub use oakcore_rs::handle::CHandle;
-
-/// addref thunk: atomically increments the count. Shared by owned and
-/// borrowed boxes — for a borrowed handle addref only extends the life of
-/// the box, not of the borrowed object.
-unsafe extern "C" fn refbox_addref(ctx: *mut std::ffi::c_void) {
- unsafe {
- let rb = ctx as *const RefBox;
- // Caller guarantees the handle is alive (ctx non-null, not yet
- // released) for the duration of the borrow.
- (*rb).refs.fetch_add(1, Ordering::Relaxed);
- }
-}
-
-/// release thunk (owned): atomically decrements; at zero the box and its
-/// value are destroyed.
-unsafe extern "C" fn refbox_release_owned(ctx: *mut std::ffi::c_void) {
- unsafe {
- let rb = ctx as *mut RefBox;
- // AcqRel: the side that reaches zero must observe all writes made
- // before the final release (including internal state the value's
- // destructor needs).
- if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
- drop(Box::from_raw(rb));
- }
- }
-}
-
-/// release thunk (borrowed, produced by [`make_borrowed`]): at zero only
-/// the box allocation is reclaimed; the value inside is forgotten — its
-/// ownership remains with the borrower.
-unsafe extern "C" fn refbox_release_borrowed(
- ctx: *mut std::ffi::c_void,
-) {
- unsafe {
- let rb = ctx as *mut RefBox;
- if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
- // Partial move: move the value out of a temporary Box, then
- // forget it so the Box drop only frees the allocation and the
- // value's destructor never runs (double-free guard).
- std::mem::forget((Box::from_raw(rb)).value);
- }
- }
-}
-
-/// Owned handle with count 1; empty on allocation failure.
-pub fn make_owned(value: T) -> CHandle {
- let rb = Box::into_raw(Box::new(RefBox {
- refs: AtomicU32::new(1),
- value,
- }));
- CHandle {
- ctx: rb as *mut std::ffi::c_void,
- addref: Some(refbox_addref::),
- release: Some(refbox_release_owned::),
- abi_version: OAKCOMMON_ABI_VERSION,
- }
-}
-
-/// Borrowed handle for an object owned elsewhere (release frees only
-/// the box).
-///
-/// The value is bit-copied into the box ("borrowing copy"); the box never
-/// runs the value's destructor — the borrower owns the original object
-/// and is responsible for destroying it.
-///
-/// # Safety
-/// Caller guarantees `ptr` outlives every derived handle and that its
-/// value is neither moved nor destroyed for the duration of the borrow.
-pub unsafe fn make_borrowed(ptr: *mut T) -> CHandle {
- if ptr.is_null() {
- return CHandle::null();
- }
- let rb = Box::into_raw(Box::new(RefBox {
- refs: AtomicU32::new(1),
- value: unsafe { std::ptr::read(ptr) },
- }));
- CHandle {
- ctx: rb as *mut std::ffi::c_void,
- addref: Some(refbox_addref::),
- release: Some(refbox_release_borrowed::),
- abi_version: OAKCOMMON_ABI_VERSION,
- }
-}
-
-/// Typed view into a handle; `None` for empty handles.
-///
-/// # Safety
-/// `T` must be the boxed type.
-pub unsafe fn get(h: &CHandle) -> Option<&T> {
- if h.is_null() {
- return None;
- }
- unsafe { Some(&(*(h.ctx as *const RefBox)).value) }
-}
-
-/// Mutable typed view into a handle; `None` for empty handles.
-///
-/// # Safety
-/// `T` must be the boxed type, and the caller must not alias the returned
-/// reference with any other live reference into the same handle.
-pub unsafe fn get_mut(h: &CHandle) -> Option<&mut T> {
- if h.is_null() {
- return None;
- }
- unsafe { Some(&mut (*(h.ctx as *mut RefBox)).value) }
-}
-
-/// Panic-catching FFI wrapper for i32-returning exports.
-pub fn guard crate::error::Result<()>>(f: F) -> i32 {
- match catch_unwind(AssertUnwindSafe(f)) {
- Ok(Ok(())) => crate::error::OAKCOMMON_OK,
- Ok(Err(e)) => e.code(),
- Err(_) => crate::error::OAKCOMMON_E_FAILED,
- }
-}
-
-/// Panic-catching FFI wrapper for handle-returning exports.
-pub fn guard_handle crate::error::Result>(f: F) -> CHandle {
- match catch_unwind(AssertUnwindSafe(f)) {
- Ok(Ok(h)) => h,
- Ok(Err(_)) | Err(_) => CHandle::null(),
- }
-}
-
-/// Panic-catching FFI wrapper for void exports.
-pub fn guard_void(f: F) {
- let _ = catch_unwind(AssertUnwindSafe(f));
-}
-
-#[cfg(test)]
-mod tests {
- use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
- use std::sync::Arc;
-
- use super::*;
-
- /// Test payload that counts how many times its destructor ran.
- struct DropCounter {
- /// Shared counter bumped by `Drop::drop`.
- drops: Arc,
- }
-
- impl DropCounter {
- /// A new counter plus its shared tally.
- fn new() -> (Self, Arc) {
- let drops = Arc::new(AtomicUsize::new(0));
- (
- DropCounter {
- drops: Arc::clone(&drops),
- },
- drops,
- )
- }
- }
-
- impl Drop for DropCounter {
- fn drop(&mut self) {
- self.drops.fetch_add(1, AtomicOrdering::SeqCst);
- }
- }
-
- /// Read the refcount behind a handle (test-only peek).
- unsafe fn refs_of(h: &CHandle) -> u32 {
- unsafe { (*(h.ctx as *const RefBox)).refs.load(Ordering::Relaxed) }
- }
-
- #[test]
- fn null_handle_is_null_and_stamped() {
- let h = CHandle::null();
- assert!(h.is_null());
- assert!(h.ctx.is_null());
- assert!(h.addref.is_none());
- assert!(h.release.is_none());
- // The shared `null()` stamps no ABI version (single-lib
- // unification; `make_owned` stamps the crate version).
- assert_eq!(h.abi_version, 0);
- }
-
- #[test]
- fn make_owned_starts_at_one_ref_and_exposes_value() {
- let (value, drops) = DropCounter::new();
- let h = make_owned(value);
- assert!(!h.is_null());
- assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
- assert!(h.addref.is_some());
- assert!(h.release.is_some());
- unsafe {
- assert_eq!(refs_of::(&h), 1);
- // get() sees the boxed value.
- let v: &DropCounter = get::(&h).unwrap();
- assert_eq!(v.drops.load(AtomicOrdering::SeqCst), 0);
- (h.release.unwrap())(h.ctx);
- }
- // Releasing the last ref destroyed the box and ran the destructor.
- assert_eq!(drops.load(AtomicOrdering::SeqCst), 1);
- }
-
- #[test]
- fn owned_addref_release_balance_then_drop_at_zero() {
- let (value, drops) = DropCounter::new();
- let h = make_owned(value);
- unsafe {
- (h.addref.unwrap())(h.ctx);
- (h.addref.unwrap())(h.ctx);
- assert_eq!(refs_of::(&h), 3);
-
- (h.release.unwrap())(h.ctx);
- assert_eq!(refs_of::(&h), 2);
- assert_eq!(drops.load(AtomicOrdering::SeqCst), 0);
- (h.release.unwrap())(h.ctx);
- assert_eq!(refs_of::(&h), 1);
- assert_eq!(drops.load(AtomicOrdering::SeqCst), 0);
- (h.release.unwrap())(h.ctx);
- }
- // Final release to zero ran the destructor exactly once.
- assert_eq!(drops.load(AtomicOrdering::SeqCst), 1);
- }
-
- #[test]
- fn borrowed_release_to_zero_does_not_run_value_destructor() {
- let (value, drops) = DropCounter::new();
- let mut value = value;
- let h = unsafe { make_borrowed(&mut value) };
- assert!(!h.is_null());
- unsafe {
- assert_eq!(refs_of::(&h), 1);
- (h.addref.unwrap())(h.ctx);
- assert_eq!(refs_of::(&h), 2);
- (h.release.unwrap())(h.ctx);
- assert_eq!(refs_of::(&h), 1);
- // Releasing the borrowed box to zero frees only the box.
- (h.release.unwrap())(h.ctx);
- }
- // The value's destructor must NOT have run; ownership stayed here.
- assert_eq!(drops.load(AtomicOrdering::SeqCst), 0);
- drop(value);
- assert_eq!(drops.load(AtomicOrdering::SeqCst), 1);
- }
-
- #[test]
- fn borrowed_handle_sees_borrowed_value_contents() {
- let mut data: u64 = 0xdead_beef;
- let h = unsafe { make_borrowed(&mut data) };
- unsafe {
- let v: &u64 = get::(&h).unwrap();
- assert_eq!(*v, 0xdead_beef);
- (h.release.unwrap())(h.ctx);
- }
- }
-
- #[test]
- fn make_borrowed_null_ptr_yields_null_handle() {
- let h = unsafe { make_borrowed::(std::ptr::null_mut()) };
- assert!(h.is_null());
- assert!(h.addref.is_none());
- assert!(h.release.is_none());
- }
-
- #[test]
- fn get_on_null_handle_is_none() {
- let h = CHandle::null();
- assert!(unsafe { get::(&h) }.is_none());
- }
-
- #[test]
- fn get_returns_typed_view_of_owned_box() {
- let h = make_owned(String::from("hello"));
- unsafe {
- let s: &String = get::(&h).unwrap();
- assert_eq!(s, "hello");
- (h.release.unwrap())(h.ctx);
- }
- }
-
- #[test]
- fn guard_maps_result_to_status_code() {
- assert_eq!(guard(|| Ok(())), crate::error::OAKCOMMON_OK);
- assert_eq!(
- guard(|| Err(crate::error::Error::Invalid)),
- crate::error::OAKCOMMON_E_INVALID
- );
- assert_eq!(
- guard(|| Err(crate::error::Error::State)),
- crate::error::OAKCOMMON_E_STATE
- );
- assert_eq!(
- guard(|| Err(crate::error::Error::Failed("x".into()))),
- crate::error::OAKCOMMON_E_FAILED
- );
- assert_eq!(
- guard(|| Err(crate::error::Error::NotFound)),
- crate::error::OAKCOMMON_E_NOT_FOUND
- );
- assert_eq!(
- guard(|| Err(crate::error::Error::NoMem)),
- crate::error::OAKCOMMON_E_NOMEM
- );
- }
-
- #[test]
- fn guard_catches_panic_as_e_failed() {
- let code = guard(|| -> crate::error::Result<()> { panic!("kaboom") });
- assert_eq!(code, crate::error::OAKCOMMON_E_FAILED);
- }
-
- #[test]
- fn guard_handle_passes_through_success() {
- let h = guard_handle(|| Ok(make_owned(42u32)));
- assert!(!h.is_null());
- unsafe {
- assert_eq!(*get::(&h).unwrap(), 42);
- (h.release.unwrap())(h.ctx);
- }
- }
-
- #[test]
- fn guard_handle_maps_err_and_panic_to_null() {
- let h = guard_handle(|| Err(crate::error::Error::NoMem));
- assert!(h.is_null());
- let h = guard_handle(|| -> crate::error::Result { panic!("kaboom") });
- assert!(h.is_null());
- }
-
- #[test]
- fn guard_void_runs_closure_and_swallows_panic() {
- let ran = Arc::new(AtomicUsize::new(0));
- let r = Arc::clone(&ran);
- guard_void(move || {
- r.fetch_add(1, AtomicOrdering::SeqCst);
- });
- assert_eq!(ran.load(AtomicOrdering::SeqCst), 1);
- // A panicking closure must not unwind across the FFI boundary.
- guard_void(|| panic!("kaboom"));
- }
-}
diff --git a/crates/oakengine/Cargo.lock b/crates/oakengine.bk/Cargo.lock
similarity index 100%
rename from crates/oakengine/Cargo.lock
rename to crates/oakengine.bk/Cargo.lock
diff --git a/crates/oakengine/Cargo.toml b/crates/oakengine.bk/Cargo.toml
similarity index 100%
rename from crates/oakengine/Cargo.toml
rename to crates/oakengine.bk/Cargo.toml
diff --git a/crates/oakengine/README.md b/crates/oakengine.bk/README.md
similarity index 100%
rename from crates/oakengine/README.md
rename to crates/oakengine.bk/README.md
diff --git a/crates/oakengine/build.rs b/crates/oakengine.bk/build.rs
similarity index 100%
rename from crates/oakengine/build.rs
rename to crates/oakengine.bk/build.rs
diff --git a/crates/oakengine/include/audio/error.h b/crates/oakengine.bk/include/audio/error.h
similarity index 100%
rename from crates/oakengine/include/audio/error.h
rename to crates/oakengine.bk/include/audio/error.h
diff --git a/crates/oakengine/include/audio/levelmeter.h b/crates/oakengine.bk/include/audio/levelmeter.h
similarity index 100%
rename from crates/oakengine/include/audio/levelmeter.h
rename to crates/oakengine.bk/include/audio/levelmeter.h
diff --git a/crates/oakengine/include/audio/manager.h b/crates/oakengine.bk/include/audio/manager.h
similarity index 100%
rename from crates/oakengine/include/audio/manager.h
rename to crates/oakengine.bk/include/audio/manager.h
diff --git a/crates/oakengine/include/audio/processor.h b/crates/oakengine.bk/include/audio/processor.h
similarity index 100%
rename from crates/oakengine/include/audio/processor.h
rename to crates/oakengine.bk/include/audio/processor.h
diff --git a/crates/oakengine/include/audio/sync.h b/crates/oakengine.bk/include/audio/sync.h
similarity index 100%
rename from crates/oakengine/include/audio/sync.h
rename to crates/oakengine.bk/include/audio/sync.h
diff --git a/crates/oakengine/include/audio/waveform.h b/crates/oakengine.bk/include/audio/waveform.h
similarity index 100%
rename from crates/oakengine/include/audio/waveform.h
rename to crates/oakengine.bk/include/audio/waveform.h
diff --git a/crates/oakengine/include/codec/conform.h b/crates/oakengine.bk/include/codec/conform.h
similarity index 100%
rename from crates/oakengine/include/codec/conform.h
rename to crates/oakengine.bk/include/codec/conform.h
diff --git a/crates/oakengine/include/codec/decoder.h b/crates/oakengine.bk/include/codec/decoder.h
similarity index 100%
rename from crates/oakengine/include/codec/decoder.h
rename to crates/oakengine.bk/include/codec/decoder.h
diff --git a/crates/oakengine/include/codec/encoder.h b/crates/oakengine.bk/include/codec/encoder.h
similarity index 100%
rename from crates/oakengine/include/codec/encoder.h
rename to crates/oakengine.bk/include/codec/encoder.h
diff --git a/crates/oakengine/include/codec/error.h b/crates/oakengine.bk/include/codec/error.h
similarity index 100%
rename from crates/oakengine/include/codec/error.h
rename to crates/oakengine.bk/include/codec/error.h
diff --git a/crates/oakengine/include/codec/format.h b/crates/oakengine.bk/include/codec/format.h
similarity index 100%
rename from crates/oakengine/include/codec/format.h
rename to crates/oakengine.bk/include/codec/format.h
diff --git a/crates/oakengine/include/codec/frame.h b/crates/oakengine.bk/include/codec/frame.h
similarity index 100%
rename from crates/oakengine/include/codec/frame.h
rename to crates/oakengine.bk/include/codec/frame.h
diff --git a/crates/oakengine/include/codec/proxy.h b/crates/oakengine.bk/include/codec/proxy.h
similarity index 100%
rename from crates/oakengine/include/codec/proxy.h
rename to crates/oakengine.bk/include/codec/proxy.h
diff --git a/crates/oakengine/include/codec/task.h b/crates/oakengine.bk/include/codec/task.h
similarity index 100%
rename from crates/oakengine/include/codec/task.h
rename to crates/oakengine.bk/include/codec/task.h
diff --git a/crates/oakengine/include/common/colortransform.h b/crates/oakengine.bk/include/common/colortransform.h
similarity index 100%
rename from crates/oakengine/include/common/colortransform.h
rename to crates/oakengine.bk/include/common/colortransform.h
diff --git a/crates/oakengine/include/common/commandlineparser.h b/crates/oakengine.bk/include/common/commandlineparser.h
similarity index 100%
rename from crates/oakengine/include/common/commandlineparser.h
rename to crates/oakengine.bk/include/common/commandlineparser.h
diff --git a/crates/oakengine/include/common/config.h b/crates/oakengine.bk/include/common/config.h
similarity index 100%
rename from crates/oakengine/include/common/config.h
rename to crates/oakengine.bk/include/common/config.h
diff --git a/crates/oakengine/include/common/current.h b/crates/oakengine.bk/include/common/current.h
similarity index 100%
rename from crates/oakengine/include/common/current.h
rename to crates/oakengine.bk/include/common/current.h
diff --git a/crates/oakengine/include/common/debug.h b/crates/oakengine.bk/include/common/debug.h
similarity index 100%
rename from crates/oakengine/include/common/debug.h
rename to crates/oakengine.bk/include/common/debug.h
diff --git a/crates/oakengine/include/common/dropworkflowbehavior.h b/crates/oakengine.bk/include/common/dropworkflowbehavior.h
similarity index 100%
rename from crates/oakengine/include/common/dropworkflowbehavior.h
rename to crates/oakengine.bk/include/common/dropworkflowbehavior.h
diff --git a/crates/oakengine/include/common/error.h b/crates/oakengine.bk/include/common/error.h
similarity index 100%
rename from crates/oakengine/include/common/error.h
rename to crates/oakengine.bk/include/common/error.h
diff --git a/crates/oakengine/include/common/ffmpegutils.h b/crates/oakengine.bk/include/common/ffmpegutils.h
similarity index 100%
rename from crates/oakengine/include/common/ffmpegutils.h
rename to crates/oakengine.bk/include/common/ffmpegutils.h
diff --git a/crates/oakengine/include/common/filefunctions.h b/crates/oakengine.bk/include/common/filefunctions.h
similarity index 100%
rename from crates/oakengine/include/common/filefunctions.h
rename to crates/oakengine.bk/include/common/filefunctions.h
diff --git a/crates/oakengine/include/common/handle.h b/crates/oakengine.bk/include/common/handle.h
similarity index 100%
rename from crates/oakengine/include/common/handle.h
rename to crates/oakengine.bk/include/common/handle.h
diff --git a/crates/oakengine/include/common/loopmode.h b/crates/oakengine.bk/include/common/loopmode.h
similarity index 100%
rename from crates/oakengine/include/common/loopmode.h
rename to crates/oakengine.bk/include/common/loopmode.h
diff --git a/crates/oakengine/include/common/miscutils.h b/crates/oakengine.bk/include/common/miscutils.h
similarity index 100%
rename from crates/oakengine/include/common/miscutils.h
rename to crates/oakengine.bk/include/common/miscutils.h
diff --git a/crates/oakengine/include/common/ocioutils.h b/crates/oakengine.bk/include/common/ocioutils.h
similarity index 100%
rename from crates/oakengine/include/common/ocioutils.h
rename to crates/oakengine.bk/include/common/ocioutils.h
diff --git a/crates/oakengine/include/common/oiioutils.h b/crates/oakengine.bk/include/common/oiioutils.h
similarity index 100%
rename from crates/oakengine/include/common/oiioutils.h
rename to crates/oakengine.bk/include/common/oiioutils.h
diff --git a/crates/oakengine/include/common/power.h b/crates/oakengine.bk/include/common/power.h
similarity index 100%
rename from crates/oakengine/include/common/power.h
rename to crates/oakengine.bk/include/common/power.h
diff --git a/crates/oakengine/include/common/qtutils.h b/crates/oakengine.bk/include/common/qtutils.h
similarity index 100%
rename from crates/oakengine/include/common/qtutils.h
rename to crates/oakengine.bk/include/common/qtutils.h
diff --git a/crates/oakengine/include/common/subtitleparams.h b/crates/oakengine.bk/include/common/subtitleparams.h
similarity index 100%
rename from crates/oakengine/include/common/subtitleparams.h
rename to crates/oakengine.bk/include/common/subtitleparams.h
diff --git a/crates/oakengine/include/common/videoparams.h b/crates/oakengine.bk/include/common/videoparams.h
similarity index 100%
rename from crates/oakengine/include/common/videoparams.h
rename to crates/oakengine.bk/include/common/videoparams.h
diff --git a/crates/oakengine/include/common/xmlutils.h b/crates/oakengine.bk/include/common/xmlutils.h
similarity index 100%
rename from crates/oakengine/include/common/xmlutils.h
rename to crates/oakengine.bk/include/common/xmlutils.h
diff --git a/crates/oakengine/include/node/block.h b/crates/oakengine.bk/include/node/block.h
similarity index 100%
rename from crates/oakengine/include/node/block.h
rename to crates/oakengine.bk/include/node/block.h
diff --git a/crates/oakengine/include/node/colormanager.h b/crates/oakengine.bk/include/node/colormanager.h
similarity index 100%
rename from crates/oakengine/include/node/colormanager.h
rename to crates/oakengine.bk/include/node/colormanager.h
diff --git a/crates/oakengine/include/node/dragger.h b/crates/oakengine.bk/include/node/dragger.h
similarity index 100%
rename from crates/oakengine/include/node/dragger.h
rename to crates/oakengine.bk/include/node/dragger.h
diff --git a/crates/oakengine/include/node/error.h b/crates/oakengine.bk/include/node/error.h
similarity index 100%
rename from crates/oakengine/include/node/error.h
rename to crates/oakengine.bk/include/node/error.h
diff --git a/crates/oakengine/include/node/factory.h b/crates/oakengine.bk/include/node/factory.h
similarity index 100%
rename from crates/oakengine/include/node/factory.h
rename to crates/oakengine.bk/include/node/factory.h
diff --git a/crates/oakengine/include/node/folder.h b/crates/oakengine.bk/include/node/folder.h
similarity index 100%
rename from crates/oakengine/include/node/folder.h
rename to crates/oakengine.bk/include/node/folder.h
diff --git a/crates/oakengine/include/node/footage.h b/crates/oakengine.bk/include/node/footage.h
similarity index 100%
rename from crates/oakengine/include/node/footage.h
rename to crates/oakengine.bk/include/node/footage.h
diff --git a/crates/oakengine/include/node/group.h b/crates/oakengine.bk/include/node/group.h
similarity index 100%
rename from crates/oakengine/include/node/group.h
rename to crates/oakengine.bk/include/node/group.h
diff --git a/crates/oakengine/include/node/keyframe.h b/crates/oakengine.bk/include/node/keyframe.h
similarity index 100%
rename from crates/oakengine/include/node/keyframe.h
rename to crates/oakengine.bk/include/node/keyframe.h
diff --git a/crates/oakengine/include/node/multicam.h b/crates/oakengine.bk/include/node/multicam.h
similarity index 100%
rename from crates/oakengine/include/node/multicam.h
rename to crates/oakengine.bk/include/node/multicam.h
diff --git a/crates/oakengine/include/node/node.h b/crates/oakengine.bk/include/node/node.h
similarity index 100%
rename from crates/oakengine/include/node/node.h
rename to crates/oakengine.bk/include/node/node.h
diff --git a/crates/oakengine/include/node/project.h b/crates/oakengine.bk/include/node/project.h
similarity index 100%
rename from crates/oakengine/include/node/project.h
rename to crates/oakengine.bk/include/node/project.h
diff --git a/crates/oakengine/include/node/sequence.h b/crates/oakengine.bk/include/node/sequence.h
similarity index 100%
rename from crates/oakengine/include/node/sequence.h
rename to crates/oakengine.bk/include/node/sequence.h
diff --git a/crates/oakengine/include/node/serializer.h b/crates/oakengine.bk/include/node/serializer.h
similarity index 100%
rename from crates/oakengine/include/node/serializer.h
rename to crates/oakengine.bk/include/node/serializer.h
diff --git a/crates/oakengine/include/node/track.h b/crates/oakengine.bk/include/node/track.h
similarity index 100%
rename from crates/oakengine/include/node/track.h
rename to crates/oakengine.bk/include/node/track.h
diff --git a/crates/oakengine/include/node/traverser.h b/crates/oakengine.bk/include/node/traverser.h
similarity index 100%
rename from crates/oakengine/include/node/traverser.h
rename to crates/oakengine.bk/include/node/traverser.h
diff --git a/crates/oakengine/include/plugin/error.h b/crates/oakengine.bk/include/plugin/error.h
similarity index 100%
rename from crates/oakengine/include/plugin/error.h
rename to crates/oakengine.bk/include/plugin/error.h
diff --git a/crates/oakengine/include/plugin/host.h b/crates/oakengine.bk/include/plugin/host.h
similarity index 100%
rename from crates/oakengine/include/plugin/host.h
rename to crates/oakengine.bk/include/plugin/host.h
diff --git a/crates/oakengine/include/plugin/instance.h b/crates/oakengine.bk/include/plugin/instance.h
similarity index 100%
rename from crates/oakengine/include/plugin/instance.h
rename to crates/oakengine.bk/include/plugin/instance.h
diff --git a/crates/oakengine/include/render/cache.h b/crates/oakengine.bk/include/render/cache.h
similarity index 100%
rename from crates/oakengine/include/render/cache.h
rename to crates/oakengine.bk/include/render/cache.h
diff --git a/crates/oakengine/include/render/cancelatom.h b/crates/oakengine.bk/include/render/cancelatom.h
similarity index 100%
rename from crates/oakengine/include/render/cancelatom.h
rename to crates/oakengine.bk/include/render/cancelatom.h
diff --git a/crates/oakengine/include/render/color.h b/crates/oakengine.bk/include/render/color.h
similarity index 100%
rename from crates/oakengine/include/render/color.h
rename to crates/oakengine.bk/include/render/color.h
diff --git a/crates/oakengine/include/render/copier.h b/crates/oakengine.bk/include/render/copier.h
similarity index 100%
rename from crates/oakengine/include/render/copier.h
rename to crates/oakengine.bk/include/render/copier.h
diff --git a/crates/oakengine/include/render/error.h b/crates/oakengine.bk/include/render/error.h
similarity index 100%
rename from crates/oakengine/include/render/error.h
rename to crates/oakengine.bk/include/render/error.h
diff --git a/crates/oakengine/include/render/manager.h b/crates/oakengine.bk/include/render/manager.h
similarity index 100%
rename from crates/oakengine/include/render/manager.h
rename to crates/oakengine.bk/include/render/manager.h
diff --git a/crates/oakengine/include/render/renderer.h b/crates/oakengine.bk/include/render/renderer.h
similarity index 100%
rename from crates/oakengine/include/render/renderer.h
rename to crates/oakengine.bk/include/render/renderer.h
diff --git a/crates/oakengine/include/render/ticket.h b/crates/oakengine.bk/include/render/ticket.h
similarity index 100%
rename from crates/oakengine/include/render/ticket.h
rename to crates/oakengine.bk/include/render/ticket.h
diff --git a/crates/oakengine/include/task/error.h b/crates/oakengine.bk/include/task/error.h
similarity index 100%
rename from crates/oakengine/include/task/error.h
rename to crates/oakengine.bk/include/task/error.h
diff --git a/crates/oakengine/include/task/manager.h b/crates/oakengine.bk/include/task/manager.h
similarity index 100%
rename from crates/oakengine/include/task/manager.h
rename to crates/oakengine.bk/include/task/manager.h
diff --git a/crates/oakengine/include/task/project.h b/crates/oakengine.bk/include/task/project.h
similarity index 100%
rename from crates/oakengine/include/task/project.h
rename to crates/oakengine.bk/include/task/project.h
diff --git a/crates/oakengine/include/task/task.h b/crates/oakengine.bk/include/task/task.h
similarity index 100%
rename from crates/oakengine/include/task/task.h
rename to crates/oakengine.bk/include/task/task.h
diff --git a/crates/oakengine/include/timeline/displaymode.h b/crates/oakengine.bk/include/timeline/displaymode.h
similarity index 100%
rename from crates/oakengine/include/timeline/displaymode.h
rename to crates/oakengine.bk/include/timeline/displaymode.h
diff --git a/crates/oakengine/include/timeline/edit.h b/crates/oakengine.bk/include/timeline/edit.h
similarity index 100%
rename from crates/oakengine/include/timeline/edit.h
rename to crates/oakengine.bk/include/timeline/edit.h
diff --git a/crates/oakengine/include/timeline/error.h b/crates/oakengine.bk/include/timeline/error.h
similarity index 100%
rename from crates/oakengine/include/timeline/error.h
rename to crates/oakengine.bk/include/timeline/error.h
diff --git a/crates/oakengine/include/timeline/marker.h b/crates/oakengine.bk/include/timeline/marker.h
similarity index 100%
rename from crates/oakengine/include/timeline/marker.h
rename to crates/oakengine.bk/include/timeline/marker.h
diff --git a/crates/oakengine/include/timeline/workarea.h b/crates/oakengine.bk/include/timeline/workarea.h
similarity index 100%
rename from crates/oakengine/include/timeline/workarea.h
rename to crates/oakengine.bk/include/timeline/workarea.h
diff --git a/crates/oakengine/include/undo/error.h b/crates/oakengine.bk/include/undo/error.h
similarity index 100%
rename from crates/oakengine/include/undo/error.h
rename to crates/oakengine.bk/include/undo/error.h
diff --git a/crates/oakengine/include/undo/undocommand.h b/crates/oakengine.bk/include/undo/undocommand.h
similarity index 100%
rename from crates/oakengine/include/undo/undocommand.h
rename to crates/oakengine.bk/include/undo/undocommand.h
diff --git a/crates/oakengine/include/undo/undostack.h b/crates/oakengine.bk/include/undo/undostack.h
similarity index 100%
rename from crates/oakengine/include/undo/undostack.h
rename to crates/oakengine.bk/include/undo/undostack.h
diff --git a/crates/oakengine/src/audio.rs b/crates/oakengine.bk/src/audio.rs
similarity index 100%
rename from crates/oakengine/src/audio.rs
rename to crates/oakengine.bk/src/audio.rs
diff --git a/crates/oakengine/src/codec.rs b/crates/oakengine.bk/src/codec.rs
similarity index 100%
rename from crates/oakengine/src/codec.rs
rename to crates/oakengine.bk/src/codec.rs
diff --git a/crates/oakengine/src/common.rs b/crates/oakengine.bk/src/common.rs
similarity index 100%
rename from crates/oakengine/src/common.rs
rename to crates/oakengine.bk/src/common.rs
diff --git a/crates/oakengine/src/deferred.rs b/crates/oakengine.bk/src/deferred.rs
similarity index 100%
rename from crates/oakengine/src/deferred.rs
rename to crates/oakengine.bk/src/deferred.rs
diff --git a/crates/oakengine/src/error.rs b/crates/oakengine.bk/src/error.rs
similarity index 100%
rename from crates/oakengine/src/error.rs
rename to crates/oakengine.bk/src/error.rs
diff --git a/crates/oakengine/src/handle.rs b/crates/oakengine.bk/src/handle.rs
similarity index 100%
rename from crates/oakengine/src/handle.rs
rename to crates/oakengine.bk/src/handle.rs
diff --git a/crates/oakengine/src/ipc.rs b/crates/oakengine.bk/src/ipc.rs
similarity index 100%
rename from crates/oakengine/src/ipc.rs
rename to crates/oakengine.bk/src/ipc.rs
diff --git a/crates/oakengine/src/lib.rs b/crates/oakengine.bk/src/lib.rs
similarity index 100%
rename from crates/oakengine/src/lib.rs
rename to crates/oakengine.bk/src/lib.rs
diff --git a/crates/oakengine/src/library.rs b/crates/oakengine.bk/src/library.rs
similarity index 100%
rename from crates/oakengine/src/library.rs
rename to crates/oakengine.bk/src/library.rs
diff --git a/crates/oakengine/src/linkage.rs b/crates/oakengine.bk/src/linkage.rs
similarity index 100%
rename from crates/oakengine/src/linkage.rs
rename to crates/oakengine.bk/src/linkage.rs
diff --git a/crates/oakengine/src/node.rs b/crates/oakengine.bk/src/node.rs
similarity index 100%
rename from crates/oakengine/src/node.rs
rename to crates/oakengine.bk/src/node.rs
diff --git a/crates/oakengine/src/plugin.rs b/crates/oakengine.bk/src/plugin.rs
similarity index 100%
rename from crates/oakengine/src/plugin.rs
rename to crates/oakengine.bk/src/plugin.rs
diff --git a/crates/oakengine/src/pods.rs b/crates/oakengine.bk/src/pods.rs
similarity index 100%
rename from crates/oakengine/src/pods.rs
rename to crates/oakengine.bk/src/pods.rs
diff --git a/crates/oakengine/src/render.rs b/crates/oakengine.bk/src/render.rs
similarity index 100%
rename from crates/oakengine/src/render.rs
rename to crates/oakengine.bk/src/render.rs
diff --git a/crates/oakengine/src/storage.rs b/crates/oakengine.bk/src/storage.rs
similarity index 100%
rename from crates/oakengine/src/storage.rs
rename to crates/oakengine.bk/src/storage.rs
diff --git a/crates/oakengine/src/stubs.rs b/crates/oakengine.bk/src/stubs.rs
similarity index 99%
rename from crates/oakengine/src/stubs.rs
rename to crates/oakengine.bk/src/stubs.rs
index 382ec6d01..97614899e 100644
--- a/crates/oakengine/src/stubs.rs
+++ b/crates/oakengine.bk/src/stubs.rs
@@ -49,7 +49,6 @@ pub mod common {
use oakcommon::colortransform::ColorTransform;
use oakcommon::configstore::ConfigStore;
- use oakcommon::handle::{get, get_mut, make_owned};
use oakcommon::ocioutils::PixelFormat;
use oakcommon::videoparams::{ColorRange, Interlacing, VideoParams, VideoType};
use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_OK};
diff --git a/crates/oakengine/src/task.rs b/crates/oakengine.bk/src/task.rs
similarity index 100%
rename from crates/oakengine/src/task.rs
rename to crates/oakengine.bk/src/task.rs
diff --git a/crates/oakengine/src/test_support/audio.rs b/crates/oakengine.bk/src/test_support/audio.rs
similarity index 100%
rename from crates/oakengine/src/test_support/audio.rs
rename to crates/oakengine.bk/src/test_support/audio.rs
diff --git a/crates/oakengine/src/test_support/codec.rs b/crates/oakengine.bk/src/test_support/codec.rs
similarity index 100%
rename from crates/oakengine/src/test_support/codec.rs
rename to crates/oakengine.bk/src/test_support/codec.rs
diff --git a/crates/oakengine/src/test_support/common/mod.rs b/crates/oakengine.bk/src/test_support/common/mod.rs
similarity index 100%
rename from crates/oakengine/src/test_support/common/mod.rs
rename to crates/oakengine.bk/src/test_support/common/mod.rs
diff --git a/crates/oakengine/src/test_support/common_smoke.rs b/crates/oakengine.bk/src/test_support/common_smoke.rs
similarity index 100%
rename from crates/oakengine/src/test_support/common_smoke.rs
rename to crates/oakengine.bk/src/test_support/common_smoke.rs
diff --git a/crates/oakengine/src/test_support/it_audio.rs b/crates/oakengine.bk/src/test_support/it_audio.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_audio.rs
rename to crates/oakengine.bk/src/test_support/it_audio.rs
diff --git a/crates/oakengine/src/test_support/it_codec.rs b/crates/oakengine.bk/src/test_support/it_codec.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_codec.rs
rename to crates/oakengine.bk/src/test_support/it_codec.rs
diff --git a/crates/oakengine/src/test_support/it_common.rs b/crates/oakengine.bk/src/test_support/it_common.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_common.rs
rename to crates/oakengine.bk/src/test_support/it_common.rs
diff --git a/crates/oakengine/src/test_support/it_export.rs b/crates/oakengine.bk/src/test_support/it_export.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_export.rs
rename to crates/oakengine.bk/src/test_support/it_export.rs
diff --git a/crates/oakengine/src/test_support/it_library.rs b/crates/oakengine.bk/src/test_support/it_library.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_library.rs
rename to crates/oakengine.bk/src/test_support/it_library.rs
diff --git a/crates/oakengine/src/test_support/it_plugin.rs b/crates/oakengine.bk/src/test_support/it_plugin.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_plugin.rs
rename to crates/oakengine.bk/src/test_support/it_plugin.rs
diff --git a/crates/oakengine/src/test_support/it_storage.rs b/crates/oakengine.bk/src/test_support/it_storage.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_storage.rs
rename to crates/oakengine.bk/src/test_support/it_storage.rs
diff --git a/crates/oakengine/src/test_support/it_task.rs b/crates/oakengine.bk/src/test_support/it_task.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_task.rs
rename to crates/oakengine.bk/src/test_support/it_task.rs
diff --git a/crates/oakengine/src/test_support/it_timeline.rs b/crates/oakengine.bk/src/test_support/it_timeline.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_timeline.rs
rename to crates/oakengine.bk/src/test_support/it_timeline.rs
diff --git a/crates/oakengine/src/test_support/it_undo.rs b/crates/oakengine.bk/src/test_support/it_undo.rs
similarity index 100%
rename from crates/oakengine/src/test_support/it_undo.rs
rename to crates/oakengine.bk/src/test_support/it_undo.rs
diff --git a/crates/oakengine/src/test_support/linkage.rs b/crates/oakengine.bk/src/test_support/linkage.rs
similarity index 100%
rename from crates/oakengine/src/test_support/linkage.rs
rename to crates/oakengine.bk/src/test_support/linkage.rs
diff --git a/crates/oakengine/src/test_support/mod.rs b/crates/oakengine.bk/src/test_support/mod.rs
similarity index 100%
rename from crates/oakengine/src/test_support/mod.rs
rename to crates/oakengine.bk/src/test_support/mod.rs
diff --git a/crates/oakengine/src/test_support/node.rs b/crates/oakengine.bk/src/test_support/node.rs
similarity index 100%
rename from crates/oakengine/src/test_support/node.rs
rename to crates/oakengine.bk/src/test_support/node.rs
diff --git a/crates/oakengine/src/test_support/plugin.rs b/crates/oakengine.bk/src/test_support/plugin.rs
similarity index 100%
rename from crates/oakengine/src/test_support/plugin.rs
rename to crates/oakengine.bk/src/test_support/plugin.rs
diff --git a/crates/oakengine/src/test_support/render.rs b/crates/oakengine.bk/src/test_support/render.rs
similarity index 100%
rename from crates/oakengine/src/test_support/render.rs
rename to crates/oakengine.bk/src/test_support/render.rs
diff --git a/crates/oakengine/src/test_support/task.rs b/crates/oakengine.bk/src/test_support/task.rs
similarity index 100%
rename from crates/oakengine/src/test_support/task.rs
rename to crates/oakengine.bk/src/test_support/task.rs
diff --git a/crates/oakengine/src/test_support/undo.rs b/crates/oakengine.bk/src/test_support/undo.rs
similarity index 100%
rename from crates/oakengine/src/test_support/undo.rs
rename to crates/oakengine.bk/src/test_support/undo.rs
diff --git a/crates/oakengine/src/testmedia.rs b/crates/oakengine.bk/src/testmedia.rs
similarity index 100%
rename from crates/oakengine/src/testmedia.rs
rename to crates/oakengine.bk/src/testmedia.rs
diff --git a/crates/oakengine/src/timeline.rs b/crates/oakengine.bk/src/timeline.rs
similarity index 100%
rename from crates/oakengine/src/timeline.rs
rename to crates/oakengine.bk/src/timeline.rs
diff --git a/crates/oakengine/src/undo.rs b/crates/oakengine.bk/src/undo.rs
similarity index 100%
rename from crates/oakengine/src/undo.rs
rename to crates/oakengine.bk/src/undo.rs
diff --git a/crates/oakengine/src/worker.rs b/crates/oakengine.bk/src/worker.rs
similarity index 100%
rename from crates/oakengine/src/worker.rs
rename to crates/oakengine.bk/src/worker.rs
diff --git a/src/actions.rs b/src/actions.rs
index 753deefac..02c6993cf 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -199,6 +199,7 @@ define_actions! {
FocusHistory { cpp: "focushistory", i18n: "menu.window.history", keys: [], route: Global, menu_id: 606 };
FocusTimeline { cpp: "focustimeline", i18n: "menu.window.timeline", keys: [], route: Global, menu_id: 607 };
FocusEffectLibrary { cpp: "focuseffectlibrary", i18n: "menu.window.effect_library", keys: [], route: Global, menu_id: 608 };
+ FocusMulticam { cpp: "focusmulticam", i18n: "menu.window.multicam", keys: [], route: Global, menu_id: 609 };
MaximizePanel { cpp: "maximizepanel", i18n: "menu.window.maximize_panel", keys: ["`"], route: Global, menu_id: 1070 };
ResetDefaultLayout { cpp: "resetdefaultlayout", i18n: "menu.window.reset_layout", keys: [], route: Global, menu_id: 1071 };
@@ -231,6 +232,32 @@ define_actions! {
SyncBySourceTime { cpp: "syncsourcetime", i18n: "timeline.context.sync_source_time", keys: [], route: FocusedPanel, menu_id: 1130 };
SyncByWaveform { cpp: "syncwaveform", i18n: "timeline.context.sync_waveform", keys: ["ctrl-shift-w"], route: FocusedPanel, menu_id: 1131 };
SyncByWaveformSpeed { cpp: "syncwaveformspeed", i18n: "timeline.context.sync_waveform_speed", keys: [], route: FocusedPanel, menu_id: 1132 };
+ // The multicam source-switch hotkeys. Like the C++ `QShortcut`s attached
+ // directly to the `MulticamWidget`, they are panel-context hotkeys, not
+ // menu items: `menu_id` is [`HIDDEN_MENU_ID`] and the menus never list
+ // them. They route to the focused panel (the multicam panel handles
+ // them; any other focused panel falls through to the no-op global
+ // handler). The digit keys switch and split the clip (the C++ plain
+ // `1`..`9`); the `secondary-` variants switch without splitting
+ // (`Ctrl+1`..`Ctrl+9`).
+ MulticamSwitch1 { cpp: "multicamswitch1", i18n: "multicam.switch_1", keys: ["1"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch2 { cpp: "multicamswitch2", i18n: "multicam.switch_2", keys: ["2"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch3 { cpp: "multicamswitch3", i18n: "multicam.switch_3", keys: ["3"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch4 { cpp: "multicamswitch4", i18n: "multicam.switch_4", keys: ["4"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch5 { cpp: "multicamswitch5", i18n: "multicam.switch_5", keys: ["5"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch6 { cpp: "multicamswitch6", i18n: "multicam.switch_6", keys: ["6"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch7 { cpp: "multicamswitch7", i18n: "multicam.switch_7", keys: ["7"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch8 { cpp: "multicamswitch8", i18n: "multicam.switch_8", keys: ["8"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitch9 { cpp: "multicamswitch9", i18n: "multicam.switch_9", keys: ["9"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit1 { cpp: "multicamswitch1nosplit", i18n: "multicam.switch_1", keys: ["secondary-1"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit2 { cpp: "multicamswitch2nosplit", i18n: "multicam.switch_2", keys: ["secondary-2"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit3 { cpp: "multicamswitch3nosplit", i18n: "multicam.switch_3", keys: ["secondary-3"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit4 { cpp: "multicamswitch4nosplit", i18n: "multicam.switch_4", keys: ["secondary-4"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit5 { cpp: "multicamswitch5nosplit", i18n: "multicam.switch_5", keys: ["secondary-5"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit6 { cpp: "multicamswitch6nosplit", i18n: "multicam.switch_6", keys: ["secondary-6"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit7 { cpp: "multicamswitch7nosplit", i18n: "multicam.switch_7", keys: ["secondary-7"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit8 { cpp: "multicamswitch8nosplit", i18n: "multicam.switch_8", keys: ["secondary-8"], route: FocusedPanel, menu_id: 0 };
+ MulticamSwitchNoSplit9 { cpp: "multicamswitch9nosplit", i18n: "multicam.switch_9", keys: ["secondary-9"], route: FocusedPanel, menu_id: 0 };
Preferences { cpp: "prefs", i18n: "menu.view.preferences", keys: ["secondary-,"], route: Global, menu_id: 305 };
// --- Help ---------------------------------------------------------------
@@ -276,8 +303,20 @@ impl ActionEntry {
}
}
-/// The registry entry bound to a menu item id, if any.
+/// The menu id of the panel-context hotkeys that have no menu item (the
+/// multicam source-switch keys). [`ActionId::menu_id`] returns it for those
+/// actions; the menus never build an item with this id, and
+/// [`entry_for_menu_id`] refuses it so a stray menu dispatch can never hit
+/// a hidden action.
+pub const HIDDEN_MENU_ID: usize = 0;
+
+/// The registry entry bound to a menu item id, if any. Menu ids
+/// [`HIDDEN_MENU_ID`] (the panel-context hotkeys' placeholder) resolve to
+/// `None`.
pub fn entry_for_menu_id(id: usize) -> Option<&'static ActionEntry> {
+ if id == HIDDEN_MENU_ID {
+ return None;
+ }
REGISTRY.iter().find(|entry| entry.action.menu_id() == id)
}
@@ -432,11 +471,15 @@ mod tests {
}
/// Every menu id is unique (the menu bar reports plain ids; a duplicate
- /// would make two items dispatch the same action).
+ /// would make two items dispatch the same action). Panel-context
+ /// hotkeys share [`HIDDEN_MENU_ID`] (no menu item) and are skipped.
#[test]
fn registry_menu_ids_are_unique() {
let mut seen = std::collections::HashSet::new();
for entry in REGISTRY {
+ if entry.menu_id() == HIDDEN_MENU_ID {
+ continue;
+ }
assert!(
seen.insert(entry.menu_id()),
"duplicate menu id {} ({})",
@@ -508,10 +551,17 @@ mod tests {
&crate::panels::timeline::clip_menu(
crate::oakui::engine::SyncEligibility::default(),
&[],
+ None,
),
&mut ids,
);
for entry in REGISTRY {
+ // The panel-context hotkeys (multicam source switches) are bound
+ // to the focused panel, not to any menu — the C++ attaches them
+ // straight to the MulticamWidget.
+ if entry.menu_id() == HIDDEN_MENU_ID {
+ continue;
+ }
assert!(
ids.contains(&entry.menu_id()),
"action {} (menu id {}) has no menu item",
diff --git a/src/app.rs b/src/app.rs
index a72629a86..63cabd539 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -64,6 +64,7 @@ use crate::panels::effect_library::EffectLibraryPanel;
use crate::panels::history::HistoryPanel;
use crate::panels::ids::*;
use crate::panels::inspector::InspectorPanel;
+use crate::panels::multicam::MulticamPanel;
use crate::panels::node_editor::NodeEditorPanel;
use crate::panels::program_viewer::ProgramViewerPanel;
use crate::panels::project_explorer::ProjectExplorerPanel;
@@ -200,6 +201,7 @@ impl PanelRegistry for AppPanelRegistry {
HISTORY => "history",
TIMELINE => "timeline",
EFFECT_LIBRARY => "effect-library",
+ MULTICAM => "multicam",
_ => return None,
}
.to_string(),
@@ -263,6 +265,12 @@ impl PanelRegistry for AppPanelRegistry {
}),
cx,
)),
+ "multicam" => Some(PanelHandle::new(
+ cx.new(|cx| {
+ MulticamPanel::new(self.engine.clone(), self.program_clock.clone(), window, cx)
+ }),
+ cx,
+ )),
_ => None,
}
}
@@ -319,6 +327,7 @@ struct ShellPanels {
history: Entity>,
timeline: Entity>,
effect_library: Entity>,
+ multicam: Entity>,
}
impl OakApp {
@@ -403,6 +412,8 @@ impl OakApp {
let history = cx.new(|cx| HistoryPanel::new(engine.clone(), window, cx));
let timeline_panel =
cx.new(|cx| TimelinePanel::new(engine.clone(), timeline.clone(), window, cx));
+ let multicam_panel =
+ cx.new(|cx| MulticamPanel::new(engine.clone(), program_clock.clone(), window, cx));
// Keep the panel entities for focused-panel command routing (the dock
// only hands back type-erased handles).
@@ -415,6 +426,7 @@ impl OakApp {
history: history.clone(),
timeline: timeline_panel.clone(),
effect_library: effect_library.clone(),
+ multicam: multicam_panel.clone(),
};
// Wire each panel's right-click menu: registry-backed items come
@@ -493,6 +505,17 @@ impl OakApp {
}),
cx,
);
+ // The multicam panel tabs behind the program viewer (the C++
+ // default is hidden; the 窗口 menu's Focus Multicam brings it
+ // forward). The program viewer stays the group's active tab.
+ dock.add_panel(
+ PanelHandle::new(multicam_panel, cx),
+ Some(DropTarget {
+ panel: Some(PROGRAM_VIEWER),
+ zone: DropZone::Center,
+ }),
+ cx,
+ );
});
// Tune the default split ratios: viewers 60% / timeline 40%, project
@@ -715,6 +738,10 @@ impl OakApp {
EFFECT_LIBRARY => self.panels.effect_library.update(cx, |panel, cx| {
panel_commands::dispatch_to(panel, action, cx)
}),
+ MULTICAM => self
+ .panels
+ .multicam
+ .update(cx, |panel, cx| panel_commands::dispatch_to(panel, action, cx)),
_ => false,
}
}
@@ -894,6 +921,7 @@ impl OakApp {
A::FocusHistory => self.focus_panel(HISTORY, cx),
A::FocusTimeline => self.focus_panel(TIMELINE, cx),
A::FocusEffectLibrary => self.focus_panel(EFFECT_LIBRARY, cx),
+ A::FocusMulticam => self.focus_panel(MULTICAM, cx),
// --- Tools -----------------------------------------------------
A::Snapping => {
let enabled = !self.timeline.read(cx).state.snap_enabled;
@@ -919,6 +947,27 @@ impl OakApp {
self.rebuild_menu_bar(cx);
}
A::ProxySettings => self.open_proxy_dialog(cx),
+ // The multicam source-switch hotkeys are scoped to the Multicam
+ // panel (the focused-panel route handles them there); a fall-through
+ // from any other focused panel is a silent no-op.
+ A::MulticamSwitch1
+ | A::MulticamSwitch2
+ | A::MulticamSwitch3
+ | A::MulticamSwitch4
+ | A::MulticamSwitch5
+ | A::MulticamSwitch6
+ | A::MulticamSwitch7
+ | A::MulticamSwitch8
+ | A::MulticamSwitch9
+ | A::MulticamSwitchNoSplit1
+ | A::MulticamSwitchNoSplit2
+ | A::MulticamSwitchNoSplit3
+ | A::MulticamSwitchNoSplit4
+ | A::MulticamSwitchNoSplit5
+ | A::MulticamSwitchNoSplit6
+ | A::MulticamSwitchNoSplit7
+ | A::MulticamSwitchNoSplit8
+ | A::MulticamSwitchNoSplit9 => {}
// --- everything else is a placeholder --------------------------
other => println!(
"[action] {} not wired yet (placeholder)",
@@ -2122,7 +2171,8 @@ fn make_menus(state: MenuState) -> Vec {
menu_item(A::FocusInspector),
menu_item(A::FocusHistory),
menu_item(A::FocusTimeline),
- menu_item(A::FocusEffectLibrary).separated(),
+ menu_item(A::FocusEffectLibrary),
+ menu_item(A::FocusMulticam).separated(),
menu_item(A::MaximizePanel),
menu_item(A::ResetDefaultLayout),
]),
@@ -2488,6 +2538,7 @@ mod tests {
&crate::panels::timeline::clip_menu(
crate::oakui::engine::SyncEligibility::default(),
&[],
+ None,
),
&mut ids,
);
diff --git a/src/i18n.rs b/src/i18n.rs
index 7b7f97a96..1aa440cc1 100644
--- a/src/i18n.rs
+++ b/src/i18n.rs
@@ -283,6 +283,7 @@ const EN: &[(&str, &str)] = &[
("menu.window.history", "History"),
("menu.window.timeline", "Timeline"),
("menu.window.effect_library", "Effect Library"),
+ ("menu.window.multicam", "Multi-Cam"),
("menu.window.maximize_panel", "Maximize Panel"),
("menu.window.reset_layout", "Reset Layout"),
// --- Tools ---
@@ -324,6 +325,18 @@ const EN: &[(&str, &str)] = &[
("panel.history", "History"),
("panel.timeline", "Timeline"),
("panel.effect_library", "Effect Library"),
+ ("panel.multicam", "Multi-Cam"),
+ // --- multicam panel ---
+ ("multicam.no_multicam", "No multi-camera clip detected"),
+ ("multicam.switch_1", "Switch to Camera 1"),
+ ("multicam.switch_2", "Switch to Camera 2"),
+ ("multicam.switch_3", "Switch to Camera 3"),
+ ("multicam.switch_4", "Switch to Camera 4"),
+ ("multicam.switch_5", "Switch to Camera 5"),
+ ("multicam.switch_6", "Switch to Camera 6"),
+ ("multicam.switch_7", "Switch to Camera 7"),
+ ("multicam.switch_8", "Switch to Camera 8"),
+ ("multicam.switch_9", "Switch to Camera 9"),
// --- effect library ---
("effect_library.hint", "Double-click to add to the selected clip"),
// --- status bar ---
@@ -701,6 +714,7 @@ const ZH: &[(&str, &str)] = &[
("menu.window.history", "历史记录"),
("menu.window.timeline", "时间线"),
("menu.window.effect_library", "效果库"),
+ ("menu.window.multicam", "多机位"),
("menu.window.maximize_panel", "最大化面板"),
("menu.window.reset_layout", "重置布局"),
// --- Tools ---
@@ -742,6 +756,18 @@ const ZH: &[(&str, &str)] = &[
("panel.history", "历史记录"),
("panel.timeline", "时间线"),
("panel.effect_library", "效果库"),
+ ("panel.multicam", "多机位"),
+ // --- multicam panel ---
+ ("multicam.no_multicam", "未检测到多机位片段"),
+ ("multicam.switch_1", "切换到机位 1"),
+ ("multicam.switch_2", "切换到机位 2"),
+ ("multicam.switch_3", "切换到机位 3"),
+ ("multicam.switch_4", "切换到机位 4"),
+ ("multicam.switch_5", "切换到机位 5"),
+ ("multicam.switch_6", "切换到机位 6"),
+ ("multicam.switch_7", "切换到机位 7"),
+ ("multicam.switch_8", "切换到机位 8"),
+ ("multicam.switch_9", "切换到机位 9"),
// --- effect library ---
("effect_library.hint", "双击添加到选中片段"),
// --- status bar ---
diff --git a/src/oakui/engine.rs b/src/oakui/engine.rs
index cec2f4c92..170cd8db3 100644
--- a/src/oakui/engine.rs
+++ b/src/oakui/engine.rs
@@ -792,11 +792,95 @@ pub trait AppEngine:
let _ = (clips, adjust_speed, cx);
}
+ // -------------------------------------------------------------------
+ // Multi-camera (the C++ MulticamWidget / timeline Multi-Cam menu):
+ // detection state for the panel, angle-frame rendering, the timeline
+ // menu's enable/disable and the source switch. Defaults degrade to "no
+ // multicam", so engines without a multicam surface keep compiling.
+ // -------------------------------------------------------------------
+
+ /// The currently detected multicam state (the panel's grid), or `None`
+ /// when there is nothing to display. The backend performs the
+ /// detection on demand (selected clip → `find_multicam`, falling back
+ /// to the clip at the program playhead), so the panel always reads a
+ /// fresh answer.
+ fn multicam_state(&self) -> Option {
+ None
+ }
+
+ /// The rendered frame of one multicam angle, when a frame for the
+ /// current playhead is cached. The panel calls this for every source it
+ /// draws; `None` means the frame is not ready (the engine schedules a
+ /// background render and notifies when it lands). The backend caches
+ /// per (multicam node, source) with an LRU cap, so a paused panel never
+ /// re-renders a cell.
+ fn multicam_angle_frame(&mut self, source: i32, cx: &mut Context) -> Option> {
+ let _ = (source, cx);
+ None
+ }
+
+ /// Whether any of `clips` can host multicam — the timeline clip menu's
+ /// enable condition (the C++ `connected_viewer()` of the clip is a
+ /// sequence).
+ fn multicam_eligible(&self, clips: &[ClipId]) -> bool {
+ let _ = clips;
+ false
+ }
+
+ /// Whether the selected clips are currently multicam-enabled — the
+ /// timeline menu's checked state.
+ fn multicam_enabled_on_selection(&self, clips: &[ClipId]) -> bool {
+ let _ = clips;
+ false
+ }
+
+ /// Enables / disables multicam on `clips` (the timeline menu's checkable
+ /// item), as ONE undo entry (`Multi-Cam Enabled On %1 Clip(s)` /
+ /// `Multi-Cam Disabled On %1 Clip(s)`). Clips whose connected viewer is
+ /// not a sequence are skipped.
+ fn multicam_enable_selected(
+ &mut self,
+ clips: Vec,
+ enabled: bool,
+ cx: &mut Context,
+ ) {
+ let _ = (clips, enabled, cx);
+ }
+
+ /// Switches the currently detected multicam to `source` (the digit
+ /// keys and grid clicks), as ONE undo entry (`Switched Multi-Camera
+ /// Source`). `split_clip` = the change applies from the playhead
+ /// forward (the clip is split first).
+ fn multicam_switch_to(&mut self, source: i32, split_clip: bool, cx: &mut Context) {
+ let _ = (source, split_clip, cx);
+ }
+
/// The display name of the engine backend ("mock" / "real"), shown in
/// the status bar.
fn backend_name(&self) -> &'static str;
}
+/// The detected multicam state the Multicam panel displays (the C++
+/// `MulticamWidget`'s `node_` / `clip_` plus the resolved source count /
+/// current source). `None` in the engine means there is no multicam to
+/// show — the panel falls back to its empty state.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct MulticamState {
+ /// The source sequence node identity (the multicam's `sequence_in` edge
+ /// target; its track list supplies the angles).
+ pub sequence_id: u64,
+ /// The multicam node identity.
+ pub node_id: u64,
+ /// The timeline clip node identity whose texture input the multicam
+ /// feeds.
+ pub clip_id: u64,
+ /// The number of angle sources (the source sequence's track count of
+ /// the multicam's `sequence_type_in` kind).
+ pub source_count: i32,
+ /// The currently selected source index (`current_in`).
+ pub current_source: i32,
+}
+
/// The lifecycle state of one footage's proxy (the UI mirror of
/// `oakcodec::proxymanager::ProxyState`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
diff --git a/src/oakui/mock.rs b/src/oakui/mock.rs
index 49e281240..d7ab3ad9a 100644
--- a/src/oakui/mock.rs
+++ b/src/oakui/mock.rs
@@ -61,10 +61,16 @@ use gpui_widgets::audio_meter::AudioMeterDataSource;
use gpui_widgets::project_explorer::{ProjectDataSource, ProjectEntry};
use gpui_widgets::viewer::PlaybackClock;
+use oakcore_rs::Rational;
+use oaknode::block::clip_input;
+use oaknode::track::TrackType;
+use oaktimeline::util::{block_clip_create, track_append_block};
+
use super::engine::{
- AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, Project,
- ScopeData, Sequence, VideoFormat,
+ AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState,
+ Project, ScopeData, Sequence, VideoFormat,
};
+use super::graphops;
use super::transport::TransportState;
/// The demo sequence length: 00:04:18:18 at 25 fps.
@@ -533,6 +539,15 @@ pub struct MockEngine {
proxy_custom: HashMap,
/// The demo's global "Use Proxy Media" switch.
use_proxy: bool,
+ /// The demo multicam graph: a real oaknode project whose source
+ /// sequence's video tracks are the angles. Created lazily so the demo
+ /// panel shows a genuine graph behind its synthetic frames — and the
+ /// switch / enable / disable commands run on the real command path
+ /// (`oaktimeline::multicam` + the global undo stack).
+ multicam_graph: Mutex