mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
- Default disk cache size 20 GB (was 100 GB); settings version 3 migrates the stored old default and keeps a size the user chose. - The cache keeps max(2 GB, 5% of the disk) free: sysinfo::disk_space (df -Pk / PowerShell DriveInfo) is read in the background from memory_tick and caps the limit (DiskCache::set_disk_space); cache.diskStats reports limitBytes. - Eviction goes down to 90% of the limit, so the LRU sort runs once per tenth of the limit written instead of on every write at the limit. - Failed writes are counted (cache.diskStats failed) and the first is logged. Also fixes #2063. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
353 lines
13 KiB
Rust
353 lines
13 KiB
Rust
use super::choices::theme_family;
|
||
use super::{BTreeMap, Deserialize, Label, Serialize, Value};
|
||
|
||
/// Unknown keys inside a page, kept so a newer version's settings survive a round trip.
|
||
pub(super) type Extra = BTreeMap<String, Value>;
|
||
|
||
macro_rules! page {
|
||
($(#[$m:meta])* $name:ident { $($(#[$fm:meta])* $f:ident : $t:ty = $d:expr),* $(,)? }) => {
|
||
$(#[$m])*
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(default, rename_all = "camelCase")]
|
||
pub struct $name {
|
||
$($(#[$fm])* pub $f: $t,)*
|
||
#[serde(flatten)]
|
||
pub extra: Extra,
|
||
}
|
||
impl Default for $name {
|
||
fn default() -> Self {
|
||
$name { $($f: $d,)* extra: Extra::new() }
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
page!(General {
|
||
/// Interface language: `system` (the operating system's, where EffectCraft has it, else
|
||
/// English), or a language code from `LANGUAGES`.
|
||
language: String = "system".into(),
|
||
/// Levels of Undo (1–99).
|
||
undo_levels: u32 = 32,
|
||
/// Path Point and Handle Size (px).
|
||
path_point_size: u32 = 5,
|
||
show_tool_tips: bool = true,
|
||
/// Create New Layers at Composition Start Time (off: at the current time).
|
||
create_layers_at_comp_start: bool = true,
|
||
switches_affect_nested_comps: bool = true,
|
||
/// Default Spatial Interpolation to Linear.
|
||
default_spatial_linear: bool = false,
|
||
preserve_constant_vertex_count: bool = true,
|
||
/// Synchronize Time of All Related Items.
|
||
sync_time_related_items: bool = true,
|
||
expression_pick_whip_compact: bool = true,
|
||
/// Create Split Layers Above Original Layer (After Effects' default).
|
||
create_split_layers_above: bool = true,
|
||
use_system_color_picker: bool = false,
|
||
/// Number of projects in File ▸ Open Recent.
|
||
recent_items: u32 = 10,
|
||
});
|
||
|
||
page!(Startup {
|
||
show_home_on_launch: bool = true,
|
||
show_home_on_open_project: bool = false,
|
||
/// After a crash, offer to open the most recent auto-save.
|
||
offer_crash_recovery: bool = true,
|
||
/// What the desktop window draws with, from the next launch: `auto` (the platform's best
|
||
/// graphics API) or `gl` (OpenGL, for drivers that crash with the others). A launch that
|
||
/// stops as its window opens switches it to `gl` (#243).
|
||
window_graphics: String = "auto".into(),
|
||
});
|
||
|
||
page!(ProjectPrefs {
|
||
/// Open a template project for File ▸ New ▸ New Project.
|
||
use_template: bool = false,
|
||
template_path: String = String::new(),
|
||
});
|
||
|
||
page!(AutoSave {
|
||
enabled: bool = true,
|
||
interval_minutes: u32 = 20,
|
||
max_versions: u32 = 5,
|
||
/// `nextToProject` (an "EffectCraft Auto-Save" folder beside the project) or `custom`.
|
||
location: String = "nextToProject".into(),
|
||
folder: String = String::new(),
|
||
save_on_render_start: bool = false,
|
||
});
|
||
|
||
page!(CompositionPrefs {
|
||
/// Motion path: `all`, `none`, `seconds`, `keyframes`.
|
||
motion_path: String = "keyframes".into(),
|
||
motion_path_seconds: f64 = 15.0,
|
||
motion_path_keyframes: u32 = 15,
|
||
show_rendering_progress: bool = true,
|
||
hardware_accelerate_panels: bool = true,
|
||
});
|
||
|
||
page!(Previews {
|
||
/// Largest downsampling while playing back: `1/2`, `1/4`, `1/8`.
|
||
adaptive_resolution_limit: String = "1/8".into(),
|
||
show_internal_wireframes: bool = false,
|
||
cache_frames_when_idle: bool = false,
|
||
fast_previews: bool = false,
|
||
/// `faster` or `moreAccurate`.
|
||
zoom_quality: String = "moreAccurate".into(),
|
||
show_gpu_info: bool = false,
|
||
/// The monitor's colour space for View ▸ Use Display Color Management: `srgb` or `p3`.
|
||
display_profile: String = "srgb".into(),
|
||
});
|
||
|
||
page!(Appearance {
|
||
/// The single theme before appearance modes (`dark`, `darker` or `light`), kept for older
|
||
/// settings files and automation: setting it picks its family's mode and theme. While the mode
|
||
/// is Dark or Light it mirrors the theme in use.
|
||
theme: String = "dark".into(),
|
||
/// `auto` (follow the operating system), `dark` or `light`. New installs stay Dark.
|
||
appearance_mode: String = "dark".into(),
|
||
/// The theme used in dark appearance: `dark` or `darker`.
|
||
dark_theme: String = "dark".into(),
|
||
/// The theme used in light appearance: `light`.
|
||
light_theme: String = "light".into(),
|
||
/// Theme shortcuts in the appearance context menu.
|
||
favorite_themes: Vec<String> = Vec::new(),
|
||
/// User interface brightness, -1 (darker) … 1 (lighter).
|
||
brightness: f64 = 0.0,
|
||
/// The size of the whole interface in percent (75–200), on top of the display's own scale.
|
||
ui_scale: u32 = 100,
|
||
use_label_color_for_handles: bool = true,
|
||
use_label_color_for_tabs: bool = true,
|
||
cycle_mask_colors: bool = true,
|
||
use_gradients: bool = true,
|
||
/// macOS: draw the menu bar inside the window instead of the system menu bar.
|
||
in_window_menu_bar_mac: bool = false,
|
||
});
|
||
|
||
page!(Grids {
|
||
grid_color: String = "#7a9cff".into(),
|
||
grid_spacing: f64 = 100.0,
|
||
grid_subdivisions: u32 = 4,
|
||
/// `lines`, `dashed` or `dots`.
|
||
grid_style: String = "lines".into(),
|
||
proportional_horizontal: u32 = 4,
|
||
proportional_vertical: u32 = 4,
|
||
guide_color: String = "#3cc8f0".into(),
|
||
guide_style: String = "lines".into(),
|
||
action_safe: f64 = 10.0,
|
||
title_safe: f64 = 20.0,
|
||
});
|
||
|
||
page!(TypePrefs {
|
||
/// `latin` or `southAsian` (South Asian and Middle Eastern).
|
||
text_engine: String = "latin".into(),
|
||
font_preview: bool = true,
|
||
recent_fonts: u32 = 10,
|
||
font_names_in_english: bool = false,
|
||
});
|
||
|
||
page!(Import {
|
||
/// Still footage duration: `compLength` or `seconds`.
|
||
still_footage: String = "compLength".into(),
|
||
still_seconds: f64 = 5.0,
|
||
sequence_fps: f64 = 30.0,
|
||
report_missing_frames: bool = true,
|
||
/// Automatically Reload Footage whose file another app changed (size or modification time),
|
||
/// checked every two seconds and when the window comes back to the front: `off`,
|
||
/// `nonSequence` (the default, as in After Effects: image sequences are left alone) or `all`.
|
||
auto_reload_footage: String = "nonSequence".into(),
|
||
/// Interpret unlabeled alpha as: `ask`, `guess`, `ignore`, `straight`, `premultiplied`.
|
||
unlabeled_alpha: String = "ask".into(),
|
||
/// Default drag import as: `footage`, `comp`, `compLayerSizes`.
|
||
drag_import_as: String = "footage".into(),
|
||
});
|
||
|
||
page!(Export {
|
||
default_output_folder: String = String::new(),
|
||
segment_sequences: bool = false,
|
||
segment_sequence_files: u32 = 700,
|
||
segment_movies: bool = false,
|
||
segment_movie_mb: u32 = 1024,
|
||
append_bits_to_name: bool = false,
|
||
});
|
||
|
||
page!(Audio {
|
||
/// Output device name (empty = the system default).
|
||
output_device: String = String::new(),
|
||
/// Output mapping: device channel for the left / right output (1-based).
|
||
output_left: u32 = 1,
|
||
output_right: u32 = 2,
|
||
preview_sample_rate: u32 = 48_000,
|
||
});
|
||
|
||
page!(Disk {
|
||
disk_cache_enabled: bool = true,
|
||
disk_cache_max_gb: u32 = 20,
|
||
disk_cache_folder: String = String::new(),
|
||
media_cache_folder: String = String::new(),
|
||
conformed_media_folder: String = String::new(),
|
||
});
|
||
|
||
page!(Memory {
|
||
ram_reserved_gb: u32 = 6,
|
||
/// Processed-layer cache budget (MB).
|
||
layer_cache_mb: u32 = 1024,
|
||
/// Decoded footage frame cache budget (MB).
|
||
media_cache_mb: u32 = 1024,
|
||
/// RAM preview (viewer frame) cache budget (MB).
|
||
preview_cache_mb: u32 = 3072,
|
||
reduce_cache_when_low: bool = true,
|
||
});
|
||
|
||
page!(Video {
|
||
enable_output: bool = false,
|
||
device: String = String::new(),
|
||
output_during_playback: bool = true,
|
||
mirror_on_monitor: bool = true,
|
||
disable_when_background: bool = false,
|
||
});
|
||
|
||
page!(ThreeD {
|
||
/// Default 3D renderer for new compositions: `classic` or `advanced`.
|
||
default_renderer: String = "classic".into(),
|
||
show_reference_axes: bool = true,
|
||
extended_viewer: bool = false,
|
||
realtime_shadows: bool = true,
|
||
});
|
||
|
||
page!(
|
||
/// Settings ▸ Roto Brush.
|
||
RotoPrefs {
|
||
/// The segmentation model Roto Brush 2.0 / 3.0 use: `classical` (built in) or a model id
|
||
/// from `effectcraft_segment::MODELS` (once installed).
|
||
model: String = "classical".into(),
|
||
}
|
||
);
|
||
|
||
page!(
|
||
/// Settings ▸ Face Tracking.
|
||
FacePrefs {
|
||
/// The model Track Mask ▸ Face Tracking uses: `classical` (built in) or a model id from
|
||
/// `effectcraft_segment::MODELS` (once installed).
|
||
model: String = "classical".into(),
|
||
}
|
||
);
|
||
|
||
page!(Scripting {
|
||
allow_scripts_write_files: bool = false,
|
||
warn_executing_files: bool = true,
|
||
enable_js_debugger: bool = false,
|
||
editor_font_size: u32 = 13,
|
||
syntax_highlighting: bool = true,
|
||
line_numbers: bool = true,
|
||
auto_complete: bool = true,
|
||
bracket_matching: bool = true,
|
||
word_wrap: bool = false,
|
||
error_banner: bool = true,
|
||
});
|
||
|
||
impl Scripting {
|
||
/// Whether `other` grants scripts the same as this: Application Scripting (file and network
|
||
/// access, the warning before executing files, the debugger). Scripts can't change these
|
||
/// ([`crate::Session::execute_for_script`]): a script could otherwise give itself the
|
||
/// access the user withheld.
|
||
pub fn same_permissions(&self, other: &Scripting) -> bool {
|
||
self.allow_scripts_write_files == other.allow_scripts_write_files
|
||
&& self.warn_executing_files == other.warn_executing_files
|
||
&& self.enable_js_debugger == other.enable_js_debugger
|
||
}
|
||
|
||
/// Take `from`'s Application Scripting settings (see [`Scripting::same_permissions`]).
|
||
pub fn keep_permissions(&mut self, from: &Scripting) {
|
||
self.allow_scripts_write_files = from.allow_scripts_write_files;
|
||
self.warn_executing_files = from.warn_executing_files;
|
||
self.enable_js_debugger = from.enable_js_debugger;
|
||
}
|
||
}
|
||
|
||
/// One editable label: name and `#rrggbb` colour.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct LabelPref {
|
||
pub name: String,
|
||
pub color: String,
|
||
}
|
||
|
||
/// A Composition Settings preset the user saved (Composition Settings ▸ Preset ▸ Save): the
|
||
/// frame size, pixel aspect and frame rate, as in After Effects' own presets.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CompPreset {
|
||
pub name: String,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub pixel_aspect: f64,
|
||
pub frame_rate: f64,
|
||
}
|
||
|
||
impl CompPreset {
|
||
/// Longest preset name kept.
|
||
pub const MAX_NAME: usize = 128;
|
||
|
||
/// Values a composition accepts (a hand-edited settings file can hold anything).
|
||
pub fn is_valid(&self) -> bool {
|
||
let size = 4..=30000;
|
||
!self.name.trim().is_empty()
|
||
&& self.name.chars().count() <= Self::MAX_NAME
|
||
&& size.contains(&self.width)
|
||
&& size.contains(&self.height)
|
||
&& self.pixel_aspect.is_finite()
|
||
&& (0.01..=100.0).contains(&self.pixel_aspect)
|
||
&& self.frame_rate.is_finite()
|
||
&& (1.0..=999.0).contains(&self.frame_rate)
|
||
}
|
||
}
|
||
|
||
/// Saved composition presets, skipping entries that don't parse instead of losing the list.
|
||
pub(super) fn lenient_comp_presets<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<CompPreset>, D::Error> {
|
||
let v = Vec::<Value>::deserialize(d)?;
|
||
Ok(v.into_iter().filter_map(|x| serde_json::from_value(x).ok()).collect())
|
||
}
|
||
|
||
pub(super) fn default_labels() -> Vec<LabelPref> {
|
||
Label::ALL.iter().skip(1).map(|l| LabelPref { name: l.name().to_string(), color: hex(l.rgb()) }).collect()
|
||
}
|
||
|
||
fn hex([r, g, b]: [u8; 3]) -> String {
|
||
format!("#{r:02x}{g:02x}{b:02x}")
|
||
}
|
||
|
||
pub(super) fn parse_hex(s: &str) -> Option<[u8; 3]> {
|
||
let h = s.trim().trim_start_matches('#');
|
||
if h.len() != 6 {
|
||
return None;
|
||
}
|
||
let p = |i: usize| u8::from_str_radix(h.get(i..i + 2)?, 16).ok();
|
||
Some([p(0)?, p(2)?, p(4)?])
|
||
}
|
||
|
||
impl Appearance {
|
||
/// Show the legacy single `theme`: fix the mode to its family and remember it as that
|
||
/// family's theme. An unknown theme changes nothing.
|
||
pub fn select_theme(&mut self) {
|
||
match theme_family(&self.theme) {
|
||
Some((mode, "darkTheme")) => {
|
||
self.appearance_mode = mode.into();
|
||
self.dark_theme = self.theme.clone();
|
||
}
|
||
Some((mode, _)) => {
|
||
self.appearance_mode = mode.into();
|
||
self.light_theme = self.theme.clone();
|
||
}
|
||
None => {}
|
||
}
|
||
}
|
||
|
||
/// The theme to show: the light or dark family's choice by the mode; Auto follows `system_light`
|
||
/// (`None` = the system said nothing: dark).
|
||
pub fn resolved_theme(&self, system_light: Option<bool>) -> &str {
|
||
let light = match self.appearance_mode.as_str() {
|
||
"light" => true,
|
||
"auto" => system_light == Some(true),
|
||
_ => false,
|
||
};
|
||
if light { &self.light_theme } else { &self.dark_theme }
|
||
}
|
||
}
|