- App no longer depends on the oakengine rlib: build.rs links the built liboakengine.dylib (+rpath, -export_dynamic, IOSurface) and src/oakui/ffi.rs declares the pure-C surface; RealEngine calls only the frozen oakengine_* C ABI - host_syms.rs provides the oakcore_*/fb_* host symbols the dylib imports via dynamic lookup - Fix Preferences dialog crash (spawn_modal reentrancy) with a regression test - Timeline toolbar and viewer transport render C++-era icons (16px grid, dark/light themes) with localized tooltips - i18n: complete en-US table, add untranslated-key detection test - New dialogs module (preferences, export, progress)
287 lines
11 KiB
Rust
287 lines
11 KiB
Rust
// 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 <http://www.gnu.org/licenses/>.
|
||
|
||
//! 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;
|
||
|
||
/// 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,
|
||
}
|
||
|
||
/// 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>;
|
||
|
||
/// 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<Self>);
|
||
|
||
/// Seek `monitor` to `frame` (clamped to the sequence).
|
||
fn request_frame(&mut self, monitor: Monitor, frame: Frame, cx: &mut gpui::Context<Self>);
|
||
|
||
/// Start playback on `monitor`.
|
||
fn play(&mut self, monitor: Monitor, cx: &mut gpui::Context<Self>);
|
||
|
||
/// Pause playback on `monitor`, leaving the playhead where it is.
|
||
fn pause(&mut self, monitor: Monitor, cx: &mut gpui::Context<Self>);
|
||
|
||
/// Step `monitor`'s playhead by `delta` frames (negative steps back).
|
||
fn step(&mut self, monitor: Monitor, delta: i64, cx: &mut gpui::Context<Self>);
|
||
|
||
/// 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<Self>);
|
||
}
|
||
|
||
/// 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<T: PlaybackClock + 'static> 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>) -> Self;
|
||
|
||
/// The source monitor's clock entity.
|
||
fn source_clock(&self) -> &Entity<Self::Clock>;
|
||
|
||
/// The program monitor's clock entity.
|
||
fn program_clock(&self) -> &Entity<Self::Clock>;
|
||
|
||
/// 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<RenderImage>;
|
||
|
||
/// 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<Self>);
|
||
|
||
/// 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<Self>);
|
||
|
||
/// Sets the row height of every timeline track (timeline toolbar).
|
||
fn set_track_height(&mut self, height: Pixels, cx: &mut Context<Self>);
|
||
|
||
/// Selects a material-bin entry (project-explorer "open").
|
||
fn select_item(&mut self, id: u64, cx: &mut Context<Self>);
|
||
|
||
/// Applies an effect-stack edit request to the engine's model.
|
||
fn apply_effect_event(&mut self, event: &EffectStackEvent, cx: &mut Context<Self>);
|
||
|
||
/// Applies a node-editor edit request to the engine's model.
|
||
fn apply_node_graph_event(&mut self, event: &NodeGraphEvent, cx: &mut Context<Self>);
|
||
|
||
/// 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<Self>);
|
||
|
||
/// Splits the clip with `clip` id at `time` (the razor action).
|
||
fn split_clip(&mut self, clip: ClipId, time: Frame, cx: &mut Context<Self>);
|
||
|
||
/// Splits every clip whose range spans the program playhead (the razor
|
||
/// tool's menu action).
|
||
fn split_at_playhead(&mut self, cx: &mut Context<Self>);
|
||
|
||
/// 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<Self>);
|
||
|
||
/// 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<Self>);
|
||
|
||
/// Steps the undo stack forward one entry.
|
||
fn redo(&mut self, cx: &mut Context<Self>);
|
||
|
||
/// Whether the project has unsaved changes.
|
||
fn project_modified(&self) -> bool;
|
||
|
||
/// Starts a new blank project with a single default sequence.
|
||
fn new_project(&mut self, cx: &mut Context<Self>);
|
||
|
||
/// 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<Self>) -> Result<(), String>;
|
||
|
||
/// Saves the project to `path` (or its own filename when `None`). The
|
||
/// format is dispatched by extension like [`open_project_path`]
|
||
/// (AppEngine::open_project_path).
|
||
fn save_project(&mut self, path: Option<PathBuf>, cx: &mut Context<Self>) -> Result<(), String>;
|
||
|
||
/// Closes the current project, leaving the app with no sequence.
|
||
fn close_project(&mut self, cx: &mut Context<Self>);
|
||
|
||
/// 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<ExportSession, String>;
|
||
|
||
/// 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<ExportEvent>,
|
||
/// Cancels the running export as soon as possible.
|
||
pub cancel: Box<dyn Fn() + Send>,
|
||
}
|