Composition Settings: save and delete your own presets

As in After Effects, the Preset menu in Composition Settings / New Composition gets a Save
button (asks for the name in a Choose Name window; an existing name of yours is replaced) and a
Delete button for the selected preset of yours. Your presets keep frame size, pixel aspect and
frame rate, are listed above the built-in ones and live in the settings (`compPresets`), so they
survive restarts and Reset All. Commands: `comp.savePreset`, `comp.deletePreset`, `comp.presets`.
A hand-edited settings file loses only its invalid presets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Anton IliashenkoandClaude Opus 5.5 committed 2026-10-12 01:16:52 +02:00
1 parent 67434e9038
commit 14b6b9c242
6 files changed
+380 -18

No files matched your search

+73 -2
View File
@@ -5,8 +5,9 @@ use effectcraft_project::Marker;
use effectcraft_time::Tick;
use serde_json::{Value, json};
use super::{CommandSpec, bad, comp_id, f_p, has_comp, has_layers, layers_p, str_p};
use crate::{EngineError, Result, Session, cmd};
use super::{CommandSpec, always, bad, comp_id, f_p, has_comp, has_layers, layers_p, str_p};
use crate::prefs::CompPreset;
use crate::{EngineError, Result, Session, cmd, query};
fn has_roi(s: &Session) -> std::result::Result<(), String> {
has_comp(s)?;
@@ -143,6 +144,65 @@ fn responsive_time(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"start": start.seconds(), "end": end.seconds()}))
}
// ---------------------------------------------------------------- composition presets
/// `comp.savePreset`: remember a frame size, pixel aspect and frame rate under a name in the
/// settings (shown in Composition Settings ▸ Preset). A preset with the same name is replaced.
fn save_preset(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "comp.savePreset";
let name = str_p(p, "name").map(str::trim).filter(|n| !n.is_empty()).ok_or_else(|| bad(C, "missing `name`"))?;
let num = |k: &str| f_p(p, k).ok_or_else(|| bad(C, format!("missing `{k}`")));
let dim = |k: &str| {
num(k).and_then(|v| {
if v.fract() == 0.0 && (0.0..=f64::from(u32::MAX)).contains(&v) { Ok(v as u32) } else { Err(bad(C, format!("`{k}` must be a whole number"))) }
})
};
let preset =
CompPreset { name: name.to_string(), width: dim("width")?, height: dim("height")?, pixel_aspect: num("pixelAspect")?, frame_rate: num("frameRate")? };
if !preset.is_valid() {
return Err(bad(
C,
format!(
"not a valid preset: width and height 4–30000 px, pixel aspect 0.01–100, frame rate 1–999, a name of at most {} characters",
CompPreset::MAX_NAME
),
));
}
let presets = &mut s.prefs.comp_presets;
let replaced = match presets.iter_mut().find(|q| q.name == preset.name) {
Some(q) => {
*q = preset;
true
}
None => {
presets.push(preset);
false
}
};
s.save_prefs();
s.prefs_revision += 1;
Ok(json!({"name": name, "replaced": replaced}))
}
/// `comp.deletePreset`: forget a saved composition preset.
fn delete_preset(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "comp.deletePreset";
let name = str_p(p, "name").ok_or_else(|| bad(C, "missing `name`"))?;
let before = s.prefs.comp_presets.len();
s.prefs.comp_presets.retain(|q| q.name != name);
if s.prefs.comp_presets.len() == before {
return Err(bad(C, format!("no saved preset named `{name}`")));
}
s.save_prefs();
s.prefs_revision += 1;
Ok(Value::Null)
}
/// `comp.presets`: the saved composition presets.
fn list_presets(s: &mut Session, _: &Value) -> Result<Value> {
serde_json::to_value(&s.prefs.comp_presets).map_err(|e| bad("comp.presets", e.to_string()))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("comp.cropToRegionOfInterest", "Crop Comp to Region of Interest", ["Composition"], None, "{comp?}", has_roi, crop_to_roi),
@@ -156,6 +216,17 @@ pub fn specs() -> Vec<CommandSpec> {
has_comp,
save_frame
),
cmd!(
"comp.savePreset",
"Save Composition Preset",
[],
None,
"{name, width, height, pixelAspect, frameRate} (replaces a saved preset with the same name)",
always,
save_preset
),
cmd!("comp.deletePreset", "Delete Composition Preset", [], None, "{name}", always, delete_preset),
query!("comp.presets", "Composition Presets", "{}", list_presets),
cmd!("comp.responsiveTime", "Responsive Design — Time", [], None, "{op: intro|outro|workArea}", has_comp, responsive_time),
]
}
+44 -2
View File
@@ -335,6 +335,42 @@ pub struct LabelPref {
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.
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())
}
fn default_labels() -> Vec<LabelPref> {
Label::ALL.iter().skip(1).map(|l| LabelPref { name: l.name().to_string(), color: hex(l.rgb()) }).collect()
}
@@ -393,6 +429,9 @@ pub struct Prefs {
pub preview: crate::preview::PreviewSettings,
/// View ▸ Simulate Output ▸ My Custom RGB.
pub custom_rgb: crate::viewer::CustomRgb,
/// Composition Settings presets the user saved, in the order saved.
#[serde(deserialize_with = "lenient_comp_presets")]
pub comp_presets: Vec<CompPreset>,
/// Top-level keys this version doesn't know (kept for newer versions).
#[serde(flatten)]
pub extra: Extra,
@@ -428,6 +467,7 @@ impl Default for Prefs {
recent_fonts: vec![],
preview: crate::preview::PreviewSettings::default(),
custom_rgb: crate::viewer::CustomRgb::default(),
comp_presets: vec![],
extra: Extra::new(),
}
}
@@ -553,6 +593,7 @@ impl Prefs {
/// Clamp values into their valid ranges and fill missing labels.
pub fn normalize(&mut self) {
self.version = PREFS_VERSION;
self.comp_presets.retain(CompPreset::is_valid);
if !is_choice(WINDOW_GRAPHICS, &self.startup.window_graphics) {
self.startup.window_graphics = Startup::default().window_graphics;
}
@@ -642,8 +683,8 @@ impl Prefs {
Ok(())
}
/// Reset one page (`general`, `labels`, `autoSave`…) or everything (`None`). Open Recent is
/// kept.
/// Reset one page (`general`, `labels`, `autoSave`…) or everything (`None`). Open Recent and
/// the saved composition presets are kept.
pub fn reset(&mut self, page: Option<&str>) -> Result<(), String> {
let d = Prefs::default();
let Some(page) = page else {
@@ -653,6 +694,7 @@ impl Prefs {
recent_presets: std::mem::take(&mut self.recent_presets),
recent_fonts: std::mem::take(&mut self.recent_fonts),
preview: std::mem::take(&mut self.preview),
comp_presets: std::mem::take(&mut self.comp_presets),
..d
};
return Ok(());
+44
View File
@@ -214,6 +214,50 @@ fn prefs_commands_get_set_reset_and_persist() {
);
}
/// Composition Settings presets the user saves live in the settings: they persist, a save with
/// an existing name replaces it, they survive Reset All, and invalid ones (from a hand-edited
/// settings file) are dropped on load.
#[test]
fn composition_presets_save_replace_delete_and_persist() {
let store = Arc::new(MemoryConfig::default());
let mut s = Session { config: Some(store.clone()), ..Default::default() };
let p = |name: &str, w: u32, fps: f64| json!({"name": name, "width": w, "height": 1440, "pixelAspect": 1.0, "frameRate": fps});
assert_eq!(s.execute_checked("comp.savePreset", p(" Edit 1440p 60 ", 2560, 60.0)).unwrap(), json!({"name": "Edit 1440p 60", "replaced": false}));
s.execute_checked("comp.savePreset", p("Vertical", 1440, 30.0)).unwrap();
assert_eq!(s.execute("comp.savePreset", p("Vertical", 1080, 30.0)).unwrap()["replaced"], json!(true));
let list = s.execute("comp.presets", json!({})).unwrap();
assert_eq!(list.as_array().unwrap().len(), 2);
assert_eq!(list[0]["name"], "Edit 1440p 60");
assert_eq!((list[0]["width"].clone(), list[0]["frameRate"].clone()), (json!(2560), json!(60.0)));
assert_eq!(list[1]["width"], json!(1080), "replaced in place");
for bad in [
p("", 1920, 30.0),
p("Tiny", 2, 30.0),
p("Fast", 1920, 5000.0),
json!({"name": "x", "width": 1920.5, "height": 1080, "pixelAspect": 1.0, "frameRate": 30}),
json!({"name": "x"}),
] {
assert!(s.execute("comp.savePreset", bad.clone()).is_err(), "{bad}");
}
// Persisted, and kept by Reset All.
let mut s2 = Session { config: Some(store.clone()), ..Default::default() };
s2.load_settings();
assert_eq!(s2.prefs.comp_presets, s.prefs.comp_presets);
s2.execute("prefs.reset", json!({})).unwrap();
assert_eq!(s2.prefs.comp_presets.len(), 2);
s2.execute_checked("comp.deletePreset", json!({"name": "Vertical"})).unwrap();
assert!(s2.execute("comp.deletePreset", json!({"name": "Vertical"})).is_err(), "already gone");
let mut s3 = Session { config: Some(store.clone()), ..Default::default() };
s3.load_settings();
assert_eq!(s3.prefs.comp_presets.iter().map(|q| q.name.as_str()).collect::<Vec<_>>(), ["Edit 1440p 60"]);
// A hand-edited settings file: the broken preset is dropped, the good one kept.
let mut v: serde_json::Value = serde_json::from_str(&store.read(PREFS_FILE).unwrap()).unwrap();
v["compPresets"].as_array_mut().unwrap().push(json!({"name": "Broken", "width": 0, "height": 1080, "pixelAspect": 1.0, "frameRate": f64::NAN}));
v["compPresets"].as_array_mut().unwrap().push(json!({"name": 7}));
let back = Prefs::from_json(&v.to_string());
assert_eq!(back.comp_presets.iter().map(|q| q.name.as_str()).collect::<Vec<_>>(), ["Edit 1440p 60"]);
}
#[test]
fn undo_levels_are_honoured() {
let mut s = session_with_comp();
+116 -11
View File
@@ -3,6 +3,8 @@
//! (`dialog.comp.<field>`).
use crate::i18n::{tr, tr_args};
use crate::icons::Icon;
use effectcraft_engine::prefs::CompPreset;
use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode};
use egui::{Color32, vec2};
use serde_json::json;
@@ -96,6 +98,8 @@ pub struct CompDraft {
/// Timecode text being edited (start, duration).
pub start_tc: Option<String>,
pub dur_tc: Option<String>,
/// The name being typed for Save Preset (the name row shows while `Some`).
pub preset_name: Option<String>,
}
impl Default for CompDraft {
@@ -121,6 +125,7 @@ impl Default for CompDraft {
tab: Tab::Basic,
start_tc: None,
dur_tc: None,
preset_name: None,
}
}
}
@@ -171,8 +176,79 @@ fn timecode_field(ui: &mut egui::Ui, buf: &mut Option<String>, secs: &mut f64, r
r
}
/// Draw the dialog. Returns `Some(params)` when OK is pressed, `Some(Null)` on Cancel.
pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registry) -> Option<serde_json::Value> {
fn name_focus_id() -> egui::Id {
egui::Id::new("comp-preset-name-focus")
}
/// Save Preset's Choose Name prompt (drawn in its own modal over the dialog while
/// `d.preset_name` is `Some`): OK or Enter saves the draft's frame size, pixel aspect and frame
/// rate under the name, replacing a saved preset with that name; Cancel or Escape closes it.
pub fn choose_name(
ui: &mut egui::Ui,
d: &mut CompDraft,
t: &Tokens,
auto: &mut Registry,
custom: &[CompPreset],
cmds: &mut Vec<(&'static str, serde_json::Value)>,
) {
let Some(mut name) = d.preset_name.take() else { return };
let mut keep = true;
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(ui.available_width()));
auto.add("dialog.comp.presetName", r.rect, "Preset Name");
if ui.data_mut(|m| m.remove_temp::<bool>(name_focus_id())).unwrap_or(false) {
r.request_focus();
}
let trimmed = name.trim();
let builtin = PRESETS.iter().any(|p| p.0 == trimmed);
let exists = custom.iter().any(|q| q.name == trimmed);
let ok = !trimmed.is_empty() && !builtin && trimmed.chars().count() <= CompPreset::MAX_NAME;
let note = if builtin {
tr("A built-in preset has this name")
} else if exists {
tr("Replaces your preset with this name")
} else {
""
};
ui.add_space(4.0);
ui.label(egui::RichText::new(note).color(Color32::GRAY));
ui.add_space(8.0);
let mut save = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
let mut cancel = ui.input(|i| i.key_pressed(egui::Key::Escape));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let b = ui.add_enabled(ok, egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
auto.add("dialog.comp.presetNameSave", b.rect, "OK");
save |= b.clicked();
let c = ui.button(tr("Cancel"));
auto.add("dialog.comp.presetNameCancel", c.rect, "Cancel");
cancel |= c.clicked();
});
if ok && save {
cmds.push(("comp.savePreset", json!({"name": trimmed, "width": d.width, "height": d.height, "pixelAspect": d.pixel_aspect, "frameRate": d.fps})));
keep = false;
}
if cancel {
keep = false;
}
if keep {
d.preset_name = Some(name);
}
}
/// Whether the draft's frame size, pixel aspect and frame rate are those of a preset.
fn matches(d: &CompDraft, w: u32, h: u32, par: f64, fps: f64) -> bool {
w == d.width && h == d.height && (par - d.pixel_aspect).abs() < 1e-3 && (fps - d.fps).abs() < 1e-3
}
/// Draw the dialog. Returns `Some(params)` when OK is pressed, `Some(Null)` on Cancel. `custom`
/// are the presets the user saved; saving or deleting one pushes its engine command to `cmds`.
pub fn show(
ui: &mut egui::Ui,
d: &mut CompDraft,
t: &Tokens,
auto: &mut Registry,
custom: &[CompPreset],
cmds: &mut Vec<(&'static str, serde_json::Value)>,
) -> Option<serde_json::Value> {
let mut out = None;
ui.horizontal(|ui| {
ui.label(tr("Composition Name:"));
@@ -195,19 +271,48 @@ pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens, auto: &mut Registr
Tab::Basic => {
egui::Grid::new("comp-basic").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| {
ui.label(tr("Preset:"));
let current = PRESETS
.iter()
.find(|(_, w, h, par, f)| *w == d.width && *h == d.height && (par - d.pixel_aspect).abs() < 1e-3 && (f - d.fps).abs() < 1e-3)
.map(|p| p.0)
// The user's own presets win over a built-in one with the same values.
let own = custom.iter().find(|q| matches(d, q.width, q.height, q.pixel_aspect, q.frame_rate));
let current = own
.map(|q| q.name.as_str())
.or_else(|| PRESETS.iter().find(|(_, w, h, par, f)| matches(d, *w, *h, *par, *f)).map(|p| p.0))
.unwrap_or("Custom");
let r = egui::ComboBox::from_id_salt("comp-preset").width(300.0).selected_text(current).show_ui(ui, |ui| {
for (name, w, h, par, f) in PRESETS {
if ui.selectable_label(*name == current, *name).clicked() {
(d.width, d.height, d.pixel_aspect, d.fps) = (*w, *h, *par, *f);
ui.horizontal(|ui| {
let r = egui::ComboBox::from_id_salt("comp-preset").width(300.0).selected_text(current).show_ui(ui, |ui| {
// Your own presets first, then the built-in ones.
for q in custom {
if ui.selectable_label(q.name == current, &q.name).clicked() {
(d.width, d.height, d.pixel_aspect, d.fps) = (q.width, q.height, q.pixel_aspect, q.frame_rate);
}
}
if !custom.is_empty() {
ui.separator();
}
for (name, w, h, par, f) in PRESETS {
if ui.selectable_label(*name == current, *name).clicked() {
(d.width, d.height, d.pixel_aspect, d.fps) = (*w, *h, *par, *f);
}
}
});
auto.add("dialog.comp.preset", r.response.rect, "Preset");
let (save_r, _) = ui.allocate_exact_size(vec2(20.0, 20.0), egui::Sense::hover());
let save = crate::widgets::icon_button(ui, save_r, Icon::Plus, d.preset_name.is_some(), t, egui::Id::new("comp-preset-save"))
.on_hover_text(tr("Save Preset…"));
auto.add("dialog.comp.presetSave", save_r, "Save Preset");
if save.clicked() {
d.preset_name = Some(own.map(|q| q.name.clone()).unwrap_or_default());
ui.data_mut(|m| m.insert_temp(name_focus_id(), true));
}
if let Some(q) = own {
let (del_r, _) = ui.allocate_exact_size(vec2(20.0, 20.0), egui::Sense::hover());
let del = crate::widgets::icon_button(ui, del_r, Icon::Trash, false, t, egui::Id::new("comp-preset-delete"))
.on_hover_text(tr("Delete Preset"));
auto.add("dialog.comp.presetDelete", del_r, "Delete Preset");
if del.clicked() {
cmds.push(("comp.deletePreset", json!({"name": q.name})));
}
}
});
auto.add("dialog.comp.preset", r.response.rect, "Preset");
ui.end_row();
ui.label(tr("Width:"));
ui.horizontal(|ui| {
+18 -3
View File
@@ -65,7 +65,8 @@ pub struct DialogState {
pub fn open_new_comp(app: &mut EffectcraftApp) {
let n = app.session.project.comps().count() + 1;
let last = app.dialog_state.last_new_comp.clone().unwrap_or_default();
app.dialog_state.comp = CompDraft { name: format!("Comp {n}"), tab: super::comp_settings::Tab::Basic, start_tc: None, dur_tc: None, ..last };
app.dialog_state.comp =
CompDraft { name: format!("Comp {n}"), tab: super::comp_settings::Tab::Basic, start_tc: None, dur_tc: None, preset_name: None, ..last };
app.dialog_state.editing_existing = false;
app.dialog = Some(Dialog::NewComp);
}
@@ -311,10 +312,24 @@ fn about_main(app: &mut EffectcraftApp, ui: &mut egui::Ui, t: &Tokens, cmd: &mut
fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, existing: bool) {
let mut d = app.dialog_state.comp.clone();
let mut result = None;
let custom = app.session.prefs.comp_presets.clone();
let mut cmds = Vec::new();
modal(ctx, "Composition Settings", vec2(600.0, 470.0), t, |ui| {
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto);
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds);
});
if result.is_none() && ui_enter(ctx) {
if d.preset_name.is_some() {
// Save Preset asks for the name in its own window over the dialog, as After Effects does.
result = None;
modal(ctx, "Choose Name", vec2(360.0, 150.0), t, |ui| super::comp_settings::choose_name(ui, &mut d, t, &mut app.auto, &custom, &mut cmds));
}
// Enter that saved a preset name (or while one is typed) doesn't also press OK.
let preset_busy = !cmds.is_empty() || d.preset_name.is_some();
for (id, params) in cmds {
if let Err(e) = app.session.execute(id, params) {
app.ui.status = e.to_string();
}
}
if result.is_none() && !preset_busy && ui_enter(ctx) {
result = Some(super::comp_settings::params(&d));
}
app.dialog_state.comp = d;
+85
View File
@@ -0,0 +1,85 @@
//! Composition Settings: saving, picking and deleting your own presets (egui_kittest).
use effectcraft_engine::Session;
use effectcraft_ui_egui::{Dialog, EffectcraftApp};
use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2};
use egui_kittest::Harness;
use serde_json::json;
fn harness() -> Harness<'static, EffectcraftApp> {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Edit", "width": 2560, "height": 1440, "frameRate": 60, "duration": 4})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
h
}
fn center(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0)
}
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
let p = center(h, id);
h.input_mut().events.push(Event::PointerMoved(p));
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.run_steps(1);
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
// The automation registry still holds the previous frame's elements: one more frame.
h.run_steps(3);
}
fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key) {
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
h.run_steps(3);
}
fn open_settings(h: &mut Harness<'_, EffectcraftApp>) {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "comp.settings", json!({})).unwrap();
h.run_steps(3);
assert_eq!(h.state().dialog, Some(Dialog::CompSettings));
}
fn preset_names(h: &Harness<'_, EffectcraftApp>) -> Vec<String> {
h.state().session.prefs.comp_presets.iter().map(|q| q.name.clone()).collect()
}
/// Save the comp's size and rate as a preset (Enter saves without pressing OK), then delete it.
#[test]
fn composition_settings_save_and_delete_your_own_preset() {
let mut h = harness();
open_settings(&mut h);
assert!(h.state().auto.find("dialog.comp.presetDelete").is_none(), "nothing of yours to delete yet");
click(&mut h, "dialog.comp.presetSave");
// Choose Name opens with the keyboard in its field.
h.input_mut().events.push(Event::Text("1440p 60".into()));
h.run_steps(2);
key(&mut h, Key::Enter);
assert_eq!(preset_names(&h), ["1440p 60"]);
let q = &h.state().session.prefs.comp_presets[0];
assert_eq!((q.width, q.height, q.pixel_aspect, q.frame_rate), (2560, 1440, 1.0, 60.0));
assert_eq!(h.state().dialog, Some(Dialog::CompSettings), "Enter in the name field doesn't press OK");
assert!(h.state().auto.find("dialog.comp.presetName").is_none(), "the name row closes");
// The comp's values are now that preset, so it can be deleted.
click(&mut h, "dialog.comp.presetDelete");
assert!(preset_names(&h).is_empty());
assert!(h.state().auto.find("dialog.comp.presetDelete").is_none());
}
/// A built-in preset's name can't be taken; Cancel closes the name row without saving.
#[test]
fn a_preset_name_cannot_be_a_built_in_one() {
let mut h = harness();
open_settings(&mut h);
click(&mut h, "dialog.comp.presetSave");
click(&mut h, "dialog.comp.presetName");
h.input_mut().events.push(Event::Text("HDTV 1080 24".into()));
h.run_steps(2);
click(&mut h, "dialog.comp.presetNameSave");
assert!(preset_names(&h).is_empty(), "the built-in name is refused");
click(&mut h, "dialog.comp.presetNameCancel");
assert!(h.state().auto.find("dialog.comp.presetName").is_none());
assert!(preset_names(&h).is_empty());
}