470 lines
14 KiB
Rust
470 lines
14 KiB
Rust
//! UI state that is not project data: tools, layout, zoom/scroll, monitor settings. Serde so the
|
|
//! control channel can read and set all of it.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::dock::{DockNode, PanelKind};
|
|
use crate::icons::Icon;
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum Tool {
|
|
#[default]
|
|
Selection,
|
|
TrackSelectForward,
|
|
TrackSelectBackward,
|
|
Ripple,
|
|
Rolling,
|
|
RateStretch,
|
|
Razor,
|
|
Slip,
|
|
Slide,
|
|
Pen,
|
|
Rectangle,
|
|
Ellipse,
|
|
Hand,
|
|
Zoom,
|
|
Type,
|
|
}
|
|
|
|
impl Tool {
|
|
pub const ALL: [Tool; 15] = [
|
|
Tool::Selection,
|
|
Tool::TrackSelectForward,
|
|
Tool::TrackSelectBackward,
|
|
Tool::Ripple,
|
|
Tool::Rolling,
|
|
Tool::RateStretch,
|
|
Tool::Razor,
|
|
Tool::Slip,
|
|
Tool::Slide,
|
|
Tool::Pen,
|
|
Tool::Rectangle,
|
|
Tool::Ellipse,
|
|
Tool::Hand,
|
|
Tool::Zoom,
|
|
Tool::Type,
|
|
];
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Tool::Selection => "Selection Tool",
|
|
Tool::TrackSelectForward => "Track Select Forward Tool",
|
|
Tool::TrackSelectBackward => "Track Select Backward Tool",
|
|
Tool::Ripple => "Ripple Edit Tool",
|
|
Tool::Rolling => "Rolling Edit Tool",
|
|
Tool::RateStretch => "Rate Stretch Tool",
|
|
Tool::Razor => "Razor Tool",
|
|
Tool::Slip => "Slip Tool",
|
|
Tool::Slide => "Slide Tool",
|
|
Tool::Pen => "Pen Tool",
|
|
Tool::Rectangle => "Rectangle Tool",
|
|
Tool::Ellipse => "Ellipse Tool",
|
|
Tool::Hand => "Hand Tool",
|
|
Tool::Zoom => "Zoom Tool",
|
|
Tool::Type => "Type Tool",
|
|
}
|
|
}
|
|
pub fn shortcut(self) -> &'static str {
|
|
match self {
|
|
Tool::Selection => "V",
|
|
Tool::TrackSelectForward => "A",
|
|
Tool::TrackSelectBackward => "Shift+A",
|
|
Tool::Ripple => "B",
|
|
Tool::Rolling => "N",
|
|
Tool::RateStretch => "R",
|
|
Tool::Razor => "C",
|
|
Tool::Slip => "Y",
|
|
Tool::Slide => "U",
|
|
Tool::Pen => "P",
|
|
Tool::Rectangle | Tool::Ellipse => "",
|
|
Tool::Hand => "H",
|
|
Tool::Zoom => "Z",
|
|
Tool::Type => "T",
|
|
}
|
|
}
|
|
pub fn icon(self) -> Icon {
|
|
match self {
|
|
Tool::Selection => Icon::Selection,
|
|
Tool::TrackSelectForward => Icon::TrackSelectFwd,
|
|
Tool::TrackSelectBackward => Icon::TrackSelectBack,
|
|
Tool::Ripple => Icon::Ripple,
|
|
Tool::Rolling => Icon::Rolling,
|
|
Tool::RateStretch => Icon::RateStretch,
|
|
Tool::Razor => Icon::Razor,
|
|
Tool::Slip => Icon::Slip,
|
|
Tool::Slide => Icon::Slide,
|
|
Tool::Pen => Icon::Pen,
|
|
Tool::Rectangle => Icon::Rectangle,
|
|
Tool::Ellipse => Icon::Ellipse,
|
|
Tool::Hand => Icon::Hand,
|
|
Tool::Zoom => Icon::Zoom,
|
|
Tool::Type => Icon::Type,
|
|
}
|
|
}
|
|
pub fn from_name(s: &str) -> Option<Tool> {
|
|
let n = s.to_ascii_lowercase().replace([' ', '_', '-'], "").replace("tool", "");
|
|
Tool::ALL
|
|
.iter()
|
|
.copied()
|
|
.find(|t| format!("{t:?}").to_ascii_lowercase() == n || t.label().to_ascii_lowercase().replace([' ', '-'], "").replace("tool", "") == n)
|
|
}
|
|
/// Tools-panel groups (Premiere groups related tools under one button with a flyout).
|
|
pub fn groups() -> Vec<Vec<Tool>> {
|
|
vec![
|
|
vec![Tool::Selection],
|
|
vec![Tool::TrackSelectForward, Tool::TrackSelectBackward],
|
|
vec![Tool::Ripple, Tool::Rolling, Tool::RateStretch],
|
|
vec![Tool::Razor],
|
|
vec![Tool::Slip, Tool::Slide],
|
|
vec![Tool::Pen, Tool::Rectangle, Tool::Ellipse],
|
|
vec![Tool::Hand, Tool::Zoom],
|
|
vec![Tool::Type],
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Playback resolution (Premiere's Full / 1/2 / 1/4 / 1/8 / 1/16).
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum PlaybackRes {
|
|
Full,
|
|
#[default]
|
|
Half,
|
|
Quarter,
|
|
Eighth,
|
|
Sixteenth,
|
|
}
|
|
|
|
impl PlaybackRes {
|
|
pub const ALL: [PlaybackRes; 5] = [PlaybackRes::Full, PlaybackRes::Half, PlaybackRes::Quarter, PlaybackRes::Eighth, PlaybackRes::Sixteenth];
|
|
pub fn scale(self) -> f32 {
|
|
match self {
|
|
PlaybackRes::Full => 1.0,
|
|
PlaybackRes::Half => 0.5,
|
|
PlaybackRes::Quarter => 0.25,
|
|
PlaybackRes::Eighth => 0.125,
|
|
PlaybackRes::Sixteenth => 0.0625,
|
|
}
|
|
}
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
PlaybackRes::Full => "Full",
|
|
PlaybackRes::Half => "1/2",
|
|
PlaybackRes::Quarter => "1/4",
|
|
PlaybackRes::Eighth => "1/8",
|
|
PlaybackRes::Sixteenth => "1/16",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Header mode (Import / Edit / Export).
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum Mode {
|
|
Import,
|
|
#[default]
|
|
Edit,
|
|
Export,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct TimelineView {
|
|
/// Pixels per second (animated toward `target_pps`).
|
|
pub pps: f64,
|
|
pub target_pps: f64,
|
|
/// Left edge time in seconds (animated toward `target_scroll`).
|
|
pub scroll: f64,
|
|
pub target_scroll: f64,
|
|
/// Vertical scroll of the video / audio halves.
|
|
pub v_scroll: f32,
|
|
pub a_scroll: f32,
|
|
/// Fraction of the track area given to video tracks.
|
|
pub split: f32,
|
|
pub video_track_h: f32,
|
|
pub audio_track_h: f32,
|
|
pub header_w: f32,
|
|
pub show_thumbnails: bool,
|
|
pub show_waveforms: bool,
|
|
/// Follow playhead during playback (page scroll).
|
|
pub follow: bool,
|
|
pub fit_pending: bool,
|
|
/// Audio tracks showing track keyframes instead of clip keyframes: track id → lane
|
|
/// (`volume`, `pan`, `mute`, `send.<i>.level`, `fx.<slot>.<param>`).
|
|
#[serde(default)]
|
|
pub track_lanes: std::collections::BTreeMap<u64, String>,
|
|
}
|
|
|
|
impl Default for TimelineView {
|
|
fn default() -> Self {
|
|
Self {
|
|
pps: 40.0,
|
|
target_pps: 40.0,
|
|
scroll: 0.0,
|
|
target_scroll: 0.0,
|
|
v_scroll: 0.0,
|
|
a_scroll: 0.0,
|
|
split: 0.5,
|
|
video_track_h: 60.0,
|
|
audio_track_h: 56.0,
|
|
header_w: 204.0,
|
|
show_thumbnails: true,
|
|
show_waveforms: true,
|
|
follow: true,
|
|
fit_pending: true,
|
|
track_lanes: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct MonitorView {
|
|
pub res: PlaybackRes,
|
|
/// Zoom: None = Fit.
|
|
pub zoom: Option<f32>,
|
|
pub safe_margins: bool,
|
|
pub show_transport: bool,
|
|
}
|
|
|
|
impl Default for MonitorView {
|
|
fn default() -> Self {
|
|
Self { res: PlaybackRes::Half, zoom: None, safe_margins: false, show_transport: true }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ProjectView {
|
|
#[default]
|
|
List,
|
|
Icon,
|
|
Freeform,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct UiState {
|
|
pub tool: Tool,
|
|
pub mode: Mode,
|
|
pub workspace: String,
|
|
pub dock: DockNode,
|
|
/// Focused panel (blue outline, receives shortcuts).
|
|
pub focused: PanelKind,
|
|
pub timeline: TimelineView,
|
|
pub program: MonitorView,
|
|
pub source: MonitorView,
|
|
pub project_view: ProjectView,
|
|
pub project_search: String,
|
|
pub effects_search: String,
|
|
pub icon_size: f32,
|
|
/// Expanded bins in the project list view.
|
|
pub expanded_bins: Vec<u64>,
|
|
/// Expanded folders in the Effects panel.
|
|
pub expanded_fx: Vec<String>,
|
|
/// Collapsed effect sections in Effect Controls ("clip:index").
|
|
pub collapsed_fx: Vec<String>,
|
|
pub show_menu_bar: bool,
|
|
pub dark: bool,
|
|
/// Lumetri scopes visible in the Program monitor area.
|
|
pub show_scopes: bool,
|
|
/// Transient status line shown in the footer.
|
|
pub status: String,
|
|
/// Export mode: chosen format id and output path.
|
|
#[serde(default)]
|
|
pub export_format: String,
|
|
#[serde(default)]
|
|
pub export_path: String,
|
|
/// Export mode: burn visible caption tracks into the video.
|
|
#[serde(default)]
|
|
pub export_burn_captions: bool,
|
|
/// Text panel: active tab ("Transcript" / "Captions" / "Graphics").
|
|
#[serde(default = "captions_tab")]
|
|
pub text_tab: String,
|
|
/// Text panel: caption search filter.
|
|
#[serde(default)]
|
|
pub caption_search: String,
|
|
/// Preferences ▸ Playback: play the rendered range when a preview render finishes.
|
|
#[serde(default = "yes")]
|
|
pub play_after_render: bool,
|
|
/// Audio Track Mixer: effects and sends section expanded.
|
|
#[serde(default)]
|
|
pub mixer_fx_open: bool,
|
|
/// Audio Gain dialog draft (mode, dB values).
|
|
#[serde(default)]
|
|
pub audio_gain: AudioGainDraft,
|
|
/// On-monitor text editing (Type tool / double-click on a text layer).
|
|
#[serde(default)]
|
|
pub gfx_edit: Option<GfxEdit>,
|
|
/// Pen tool: path points placed so far (sequence pixels).
|
|
#[serde(default)]
|
|
pub pen_points: Vec<[f64; 2]>,
|
|
/// Link Media dialog (open when Some).
|
|
#[serde(default)]
|
|
pub link_media: Option<LinkMediaDraft>,
|
|
/// Create Proxies dialog (open when Some).
|
|
#[serde(default)]
|
|
pub create_proxies: Option<ProxyDraft>,
|
|
/// Project Manager dialog (open when Some).
|
|
#[serde(default)]
|
|
pub project_manager: Option<ProjectManagerDraft>,
|
|
/// Make Offline dialog: Some(delete files?) while open.
|
|
#[serde(default)]
|
|
pub make_offline: Option<bool>,
|
|
}
|
|
|
|
/// File ▸ Link Media… (shown automatically when a project opens with missing media).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct LinkMediaDraft {
|
|
/// Selected row (index into the missing list).
|
|
pub row: usize,
|
|
pub file_name: bool,
|
|
pub extension: bool,
|
|
pub clip_id: bool,
|
|
pub duration: bool,
|
|
pub media_start: bool,
|
|
pub metadata: bool,
|
|
pub align_timecode: bool,
|
|
pub relink_others: bool,
|
|
/// Search: folder and "Display only exact name matches".
|
|
pub folder: String,
|
|
pub exact_name: bool,
|
|
/// Search results for the selected row: (path, ok, identity match, problems).
|
|
pub candidates: Vec<(String, bool, Option<bool>, String)>,
|
|
pub candidate: Option<usize>,
|
|
/// Rows the user set offline in this session of the dialog (item ids).
|
|
pub skipped: Vec<u64>,
|
|
pub message: String,
|
|
}
|
|
|
|
impl Default for LinkMediaDraft {
|
|
fn default() -> Self {
|
|
Self {
|
|
row: 0,
|
|
file_name: true,
|
|
extension: true,
|
|
clip_id: true,
|
|
duration: true,
|
|
media_start: false,
|
|
metadata: true,
|
|
align_timecode: false,
|
|
relink_others: true,
|
|
folder: String::new(),
|
|
exact_name: true,
|
|
candidates: vec![],
|
|
candidate: None,
|
|
skipped: vec![],
|
|
message: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clip ▸ Proxy ▸ Create Proxies….
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct ProxyDraft {
|
|
pub items: Vec<u64>,
|
|
pub preset: String,
|
|
/// Empty = a Proxies folder next to the original media.
|
|
pub destination: String,
|
|
}
|
|
|
|
/// File ▸ Project Manager….
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct ProjectManagerDraft {
|
|
pub sequences: Vec<u64>,
|
|
/// "collect" | "consolidate"
|
|
pub mode: String,
|
|
pub preset: String,
|
|
pub exclude_unused: bool,
|
|
pub handles: u32,
|
|
pub include_proxies: bool,
|
|
pub include_previews: bool,
|
|
pub destination: String,
|
|
/// Last dry-run estimate: (original bytes, resulting bytes, files).
|
|
pub estimate: Option<(u64, u64, usize)>,
|
|
pub message: String,
|
|
}
|
|
|
|
impl Default for ProjectManagerDraft {
|
|
fn default() -> Self {
|
|
Self {
|
|
sequences: vec![],
|
|
mode: "collect".into(),
|
|
preset: "prores_lt".into(),
|
|
exclude_unused: true,
|
|
handles: 30,
|
|
include_proxies: true,
|
|
include_previews: false,
|
|
destination: String::new(),
|
|
estimate: None,
|
|
message: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The Audio Gain dialog (Clip ▸ Audio Gain…, G).
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct AudioGainDraft {
|
|
/// "set" | "adjust" | "normalizeMax" | "normalizeAll"
|
|
pub mode: String,
|
|
pub set_db: f64,
|
|
pub adjust_db: f64,
|
|
pub max_peak_db: f64,
|
|
pub all_peaks_db: f64,
|
|
}
|
|
|
|
/// The text layer being edited on the Program monitor: caret and selection anchor are byte
|
|
/// offsets into the layer's text.
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct GfxEdit {
|
|
pub clip: u64,
|
|
pub layer: usize,
|
|
pub caret: usize,
|
|
pub anchor: usize,
|
|
}
|
|
|
|
fn captions_tab() -> String {
|
|
"Captions".into()
|
|
}
|
|
|
|
fn yes() -> bool {
|
|
true
|
|
}
|
|
|
|
impl Default for UiState {
|
|
fn default() -> Self {
|
|
Self {
|
|
tool: Tool::Selection,
|
|
mode: Mode::Edit,
|
|
workspace: "Editing".into(),
|
|
dock: crate::dock::workspace("Editing"),
|
|
focused: PanelKind::Timeline,
|
|
timeline: TimelineView::default(),
|
|
program: MonitorView::default(),
|
|
source: MonitorView::default(),
|
|
project_view: ProjectView::Icon,
|
|
project_search: String::new(),
|
|
effects_search: String::new(),
|
|
icon_size: 110.0,
|
|
expanded_bins: vec![],
|
|
expanded_fx: vec!["Video Transitions".into(), "Video Transitions/Dissolve".into()],
|
|
collapsed_fx: vec![],
|
|
show_menu_bar: true,
|
|
dark: true,
|
|
show_scopes: false,
|
|
status: String::new(),
|
|
export_format: "h264".into(),
|
|
export_path: String::new(),
|
|
export_burn_captions: false,
|
|
text_tab: captions_tab(),
|
|
caption_search: String::new(),
|
|
play_after_render: true,
|
|
mixer_fx_open: false,
|
|
audio_gain: AudioGainDraft::default(),
|
|
gfx_edit: None,
|
|
pen_points: vec![],
|
|
link_media: None,
|
|
create_proxies: None,
|
|
project_manager: None,
|
|
make_offline: None,
|
|
}
|
|
}
|
|
}
|