Files
oak-editor/src/oakui/engine.rs
T
Mike-Solar 063837446d feat(app): link liboakengine.dylib via frozen C ABI; UI fixes
- 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)
2026-08-11 00:25:20 +08:00

287 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>,
}