// 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 .
//! The engine gateway: the narrow Rust API through which the app layer talks
//! to the engine.
//!
//! # Why a gateway trait
//!
//! The UI must never depend on *how* the engine is implemented. Today the
//! only implementation is the mock ([`super::mock::MockEngine`]) feeding demo
//! data; later a real backend will bind the `liboakengine` C ABI
//! (`src/facade/rust`, the frozen `oakengine_*` exports) behind the *same*
//! trait. Swapping backends then touches only the wiring in
//! [`crate::app`] — the panels, the widgets and the view state stay as they
//! are.
//!
//! The trait is intentionally narrow: open a project, inspect the current
//! sequence, and drive the transport (play / pause / step / seek). Timeline
//! edits arrive as widget request events and are applied by the host through
//! methods on the engine type itself (see the `MockEngine` docs for the
//! current mapping), so they do not need to be part of this seam yet.
//!
//! Everything here is plain Rust — no C ABI, no FFI. The C-ABI binding is a
//! later concern of the real backend only.
use std::path::PathBuf;
use std::sync::Arc;
use gpui::effect_stack::{EffectStackDataSource, EffectStackEvent};
use gpui::node_graph::{NodeGraphDataSource, NodeGraphEvent};
use gpui::timeline::{ClipId, Frame, FrameRate, TimelineDataSource, TimelineEvent, TrackKind};
use gpui::{App, Context, Entity, Pixels, RenderImage};
use gpui_widgets::audio_meter::AudioMeterDataSource;
use gpui_widgets::project_explorer::ProjectDataSource;
use gpui_widgets::viewer::PlaybackClock;
pub use super::scopes::ScopeData;
/// A monitor the transport can address.
///
/// Oak has two independent transports: the source monitor plays the clip
/// shown in the source viewer (素材查看器), the program monitor plays the
/// sequence shown in the program viewer (序列查看器).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Monitor {
/// The source (footage) monitor.
Source,
/// The program (sequence) monitor.
Program,
}
/// A video format: resolution plus frame rate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VideoFormat {
/// Width in pixels.
pub width: u32,
/// Height in pixels.
pub height: u32,
/// The frame rate (rational, e.g. 30000/1001 for NTSC 29.97).
pub rate: FrameRate,
}
impl VideoFormat {
/// The classic HD television format: 1920×1080 at 25 fps.
pub fn hd_1080p25() -> Self {
Self {
width: 1920,
height: 1080,
rate: FrameRate::new(25, 1),
}
}
}
/// A project open in the engine.
#[derive(Debug, Clone, PartialEq)]
pub struct Project {
/// The project's display name.
pub name: String,
/// The project file on disk (`.ove`).
pub path: PathBuf,
}
/// A project-library row, as the project manager lists it (M13 D4). The
/// stats are derived from the row's head state by the backend (they are
/// never stored in the library).
#[derive(Debug, Clone, PartialEq)]
pub struct LibraryProject {
/// The library row uuid (the open / rename / duplicate / delete /
/// export selector).
pub uuid: String,
/// The row's display name.
pub name: String,
/// Row creation time (unix seconds, UTC).
pub created_at: i64,
/// Last-write time (unix seconds, UTC; the manager sort key).
pub modified_at: i64,
/// Longest sequence duration in milliseconds.
pub duration_ms: i64,
/// Total tracks across all sequences.
pub track_count: i32,
/// Total clip blocks.
pub clip_count: i32,
/// Total footage nodes.
pub footage_count: i32,
}
/// The sequence currently open in the project.
#[derive(Debug, Clone, PartialEq)]
pub struct Sequence {
/// The sequence's display name.
pub name: String,
/// The sequence's video format.
pub format: VideoFormat,
/// The sequence length in frames.
pub length: Frame,
}
/// The engine gateway.
///
/// Implementations own the "engine" side of the app: project state, the
/// current sequence, and the transport. Query methods are pure reads;
/// mutating methods take a gpui [`Context`](gpui::Context) so the backend can
/// update its observable entities (clocks, models) and notify them.
pub trait EngineGateway: Sized {
/// The currently open project, or `None` before any project is opened.
fn project(&self) -> Option<&Project>;
/// The current sequence of the open project, if any.
fn current_sequence(&self) -> Option<&Sequence>;
/// The display name of the source media shown in the source viewer, used
/// in the viewer header and dock tab. Empty when the engine has no source
/// media loaded.
fn source_media_name(&self) -> String {
String::new()
}
/// Open a project file. The backend loads it and becomes the source of
/// truth for [`project`](EngineGateway::project) /
/// [`current_sequence`](EngineGateway::current_sequence).
fn open_project(&mut self, path: PathBuf, cx: &mut gpui::Context);
/// Seek `monitor` to `frame` (clamped to the sequence).
fn request_frame(&mut self, monitor: Monitor, frame: Frame, cx: &mut gpui::Context);
/// Start playback on `monitor`.
fn play(&mut self, monitor: Monitor, cx: &mut gpui::Context);
/// Pause playback on `monitor`, leaving the playhead where it is.
fn pause(&mut self, monitor: Monitor, cx: &mut gpui::Context);
/// Step `monitor`'s playhead by `delta` frames (negative steps back).
fn step(&mut self, monitor: Monitor, delta: i64, cx: &mut gpui::Context);
/// Advance the playback clocks by one wall-clock tick. Called on a
/// periodic timer while any monitor is playing.
fn tick(&mut self, cx: &mut gpui::Context);
}
/// The transport clock type an engine drives its monitors with.
///
/// Each engine owns two clocks (source + program), one per
/// [`Monitor`], and exposes them to the viewer widgets through the
/// [`PlaybackClock`] trait.
pub trait EngineClock: PlaybackClock + 'static {}
impl EngineClock for T {}
/// The full app-facing engine surface: the gateway plus every widget
/// data-source trait and the app-only operations (clocks, viewer frames,
/// edits, undo/redo, file operations).
///
/// The app shell (`crate::app::OakApp`) and every panel are generic over
/// `E: AppEngine`, so swapping the backend (mock vs real) is a one-line
/// choice at startup — see [`crate::app::run`].
pub trait AppEngine:
EngineGateway
+ TimelineDataSource
+ EffectStackDataSource
+ NodeGraphDataSource
+ ProjectDataSource
+ AudioMeterDataSource
{
/// The concrete transport-clock type (see [`EngineClock`]).
type Clock: EngineClock;
/// Builds a fresh engine instance (no project open, or demo data for
/// the mock).
fn create(cx: &mut Context) -> Self;
/// The source monitor's clock entity.
fn source_clock(&self) -> &Entity;
/// The program monitor's clock entity.
fn program_clock(&self) -> &Entity;
/// The current playhead frame of `monitor`'s clock.
fn clock_frame(&self, monitor: Monitor, cx: &App) -> Frame;
/// The CPU frame the viewers display for `monitor` (cached per playhead
/// frame, so a paused viewer never regenerates its picture).
fn cpu_frame(&self, monitor: Monitor, cx: &App) -> Arc;
/// The scope samples ([`ScopeData`]) of `monitor`'s current CPU frame.
/// The analysis runs inside the frame render pass (cached per playhead
/// frame alongside the image), so this read is an `Arc` clone and never
/// re-walks the frame.
fn scope_data(&self, monitor: Monitor, cx: &App) -> ScopeData;
/// Adds a new empty track of the given kind (undoable where the backend
/// supports it).
fn add_track(&mut self, kind: TrackKind, cx: &mut Context);
/// Removes the track at display `index` (the index into
/// [`TimelineDataSource::track`]; undoable where the backend supports
/// it).
fn remove_track(&mut self, index: usize, cx: &mut Context);
/// Sets the row height of every timeline track (timeline toolbar).
fn set_track_height(&mut self, height: Pixels, cx: &mut Context);
/// Selects a material-bin entry (project-explorer "open").
fn select_item(&mut self, id: u64, cx: &mut Context);
/// Applies an effect-stack edit request to the engine's model.
fn apply_effect_event(&mut self, event: &EffectStackEvent, cx: &mut Context);
/// Updates the timeline clip selection (drives the effect stack's
/// target). The app shell forwards `TimelineEvent::SelectionChanged`
/// with the view's selection set. Default: no-op (engines without a
/// selection-driven stack keep their existing behavior).
fn set_selected_clips(&mut self, _clips: Vec, _cx: &mut Context) {}
/// The effect types the user can add to the selected clip's chain, as
/// (type id, display name) pairs — the facade factory entries flagged
/// `video_effect` and not hidden from the create menu. The inspector
/// panel lists them in its "add effect" menu. Default: empty.
fn addable_effects(&self) -> Vec<(String, String)> {
Vec::new()
}
/// Inserts the effect `type_id` at `index` into the selected clip's
/// chain (undoable). `index` is an insertion index into
/// [`EffectStackDataSource::effects`] (0 = closest to the source);
/// the panel passes the position carried by the `AddRequested` event.
/// Returns a user-facing error message on failure. Default: unsupported.
fn add_effect(
&mut self,
index: usize,
type_id: &str,
cx: &mut Context,
) -> Result<(), String> {
let _ = (index, type_id, cx);
Err("add effect not supported".into())
}
/// Applies a node-editor edit request to the engine's model.
fn apply_node_graph_event(&mut self, event: &NodeGraphEvent, cx: &mut Context);
/// Applies a timeline widget edit request (trim / move / playhead) to the
/// engine's model. Edits are applied through the backend's edit commands
/// with undo packaging; the playhead change is a plain seek.
fn apply_timeline_event(&mut self, event: &TimelineEvent, cx: &mut Context);
/// Splits the clip with `clip` id at `time` (the razor action).
fn split_clip(&mut self, clip: ClipId, time: Frame, cx: &mut Context);
/// Splits every clip whose range spans the program playhead (the razor
/// tool's menu action).
fn split_at_playhead(&mut self, cx: &mut Context);
/// Deletes the clip with `clip` id, rippling following content left when
/// `ripple` is set.
fn delete_clip(&mut self, clip: ClipId, ripple: bool, cx: &mut Context);
/// Whether the undo stack has an entry to undo.
fn can_undo(&self) -> bool;
/// Whether the undo stack has an entry to redo.
fn can_redo(&self) -> bool;
/// Steps the undo stack back one entry.
fn undo(&mut self, cx: &mut Context);
/// Steps the undo stack forward one entry.
fn redo(&mut self, cx: &mut Context);
/// Starts a new blank project with a single default sequence.
fn new_project(&mut self, cx: &mut Context);
/// Opens a project file. The format is dispatched by extension: `.ove`
/// through the OVE serializer, `.otio` / `.fcpxml` through the oaktask
/// interchange loader.
fn open_project_path(&mut self, path: PathBuf, cx: &mut Context) -> Result<(), String>;
/// Exports the current project to the file `path` (the 导出工程文件…
/// action's target). The format is dispatched by extension like
/// [`open_project_path`] (AppEngine::open_project_path). Exporting is a
/// pure file write — the write-through library already persists every
/// edit, so there is no "save" anymore.
fn export_project_path(&mut self, path: PathBuf, cx: &mut Context)
-> Result<(), String>;
/// Closes the current project, leaving the app with no sequence.
fn close_project(&mut self, cx: &mut Context);
// -------------------------------------------------------------------
// Project library (M13 D4: the write-through database the manager
// window browses). Default: unsupported (empty list / error strings).
// -------------------------------------------------------------------
/// Whether the open project is bound to the library write-through
/// session (the status bar's write state).
fn storage_bound(&self) -> bool {
false
}
/// The last write-through / snapshot error of the open project, if any.
fn storage_last_error(&self) -> Option {
None
}
/// Lists the project library, most recently modified first (the
/// project manager's data source).
fn library_projects(&self) -> Result, String> {
Err("project library not supported".into())
}
/// Creates a blank project named `name` in the library and opens it.
fn library_create_project(&mut self, name: &str, cx: &mut Context) -> Result<(), String> {
let _ = (name, cx);
Err("project library not supported".into())
}
/// Opens the library project `uuid` (closing the current project).
fn library_open_project(&mut self, uuid: &str, cx: &mut Context) -> Result<(), String> {
let _ = (uuid, cx);
Err("project library not supported".into())
}
/// Deletes the library project `uuid` (the manager confirms first).
fn library_delete_project(&mut self, uuid: &str) -> Result<(), String> {
let _ = uuid;
Err("project library not supported".into())
}
/// Renames the library project `uuid` (the manager's list name).
fn library_rename_project(&mut self, uuid: &str, name: &str) -> Result<(), String> {
let _ = (uuid, name);
Err("project library not supported".into())
}
/// Duplicates the library project `uuid` (history included) under a
/// fresh uuid.
fn library_duplicate_project(&mut self, uuid: &str) -> Result<(), String> {
let _ = uuid;
Err("project library not supported".into())
}
/// Imports a `.ove` / `.otio` / `.fcpxml` project file into the library
/// as a new row; returns the new row's uuid.
fn library_import_project(&mut self, path: PathBuf) -> Result {
let _ = path;
Err("project library not supported".into())
}
/// Exports the library project `uuid` to `path`; the format is
/// dispatched by extension (`.ove` / `.otio` / `.fcpxml`).
fn library_export_project(&mut self, uuid: &str, path: PathBuf) -> Result<(), String> {
let _ = (uuid, path);
Err("project library not supported".into())
}
/// The timeline waveform cache (M12 P4); `None` when the backend
/// does not provide waveforms.
fn waveform_cache(&self) -> Option> {
None
}
/// Import a media file into the project (M12 P3). Default: no-op.
fn import_footage(
&mut self,
_path: std::path::PathBuf,
_cx: &mut Context,
) -> Result<(), String> {
Ok(())
}
/// Starts an export of the current sequence in `format` to `path` and
/// returns a session the host polls for progress and can cancel.
///
/// The export runs on a background thread; the returned
/// [`ExportSession`] carries the event channel and the cancel handle.
fn start_export(&mut self, format: i32, path: PathBuf) -> Result;
/// The display name of the engine backend ("mock" / "real"), shown in
/// the status bar.
fn backend_name(&self) -> &'static str;
}
/// A single progress event from a running export task.
#[derive(Debug, Clone, PartialEq)]
pub enum ExportEvent {
/// The task started.
Started,
/// Fraction done, in `0.0..=1.0`.
Progress(f64),
/// The task finished. `true` = succeeded; the string carries the failure
/// message on error.
Finished(bool, String),
}
/// A running export: the event channel the host drains plus the cancel
/// handle. Dropping the session does not abort the export thread; the
/// thread owns the task and frees it when it finishes.
pub struct ExportSession {
/// The event receiver (the background thread's sender lives as long as
/// the session's `cancel` side, so a dropped receiver just stops
/// delivering).
pub events: std::sync::mpsc::Receiver,
/// Cancels the running export as soon as possible.
pub cancel: Box,
}