mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-03 00:59:00 +00:00
#1436: Solid, Camera, Light and Composition Settings preview live
The Gradient Editor's Preview now covers the other settings dialogs that edit something that exists: Solid Settings on a solid layer, Camera Settings, Light Settings and Composition Settings. A change of the draft reaches the project at once, so the canvas shows it while it is made. - One shared mechanism (`panels::live_preview`): each step goes back to a checkpoint of the project and undo history taken when the dialog opened, then runs the dialog's command once. So the project is always "as it opened, plus one command": OK leaves one undo step, Cancel and Escape restore the checkpoint, Preview off applies nothing until OK, and switching it off takes back what was applied. - A previewing dialog opens at the lower right, without the dark backdrop, and the window behind it is no longer greyed out; it can still be dragged by its title. The same dialogs creating something new (New Solid, New Camera, New Light, New Composition) are unchanged and have no Preview. - `Placement` is public, and `EffectcraftApp::dialog_state` gives tests and automation the drafts. Claude-Session: https://claude.ai/code/session_013fTnVxnXTv2MBW6uj2PPyU
This commit is contained in:
11 files changed
+568
-82
No files matched your search
+16
-10
@@ -101,14 +101,6 @@ pub enum Dialog {
|
||||
DeleteTemplate,
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
/// The dialog edits what is behind it live (Preview), so the window stays at full brightness
|
||||
/// while it is open: clicks still don't reach it, but it is not greyed out (After Effects).
|
||||
pub fn leaves_window_undimmed(self) -> bool {
|
||||
matches!(self, Dialog::GradientEditor)
|
||||
}
|
||||
}
|
||||
|
||||
/// The desktop's light or dark appearance where egui cannot see it (Wayland compositors that send
|
||||
/// winit no theme). `None` = the host does not know.
|
||||
pub type SystemThemeFn = Box<dyn Fn(&egui::Context) -> Option<egui::Theme>>;
|
||||
@@ -578,6 +570,18 @@ impl EffectcraftApp {
|
||||
&mut self.dialog_state.gradient
|
||||
}
|
||||
|
||||
/// The open settings dialog's draft and Preview state (`panels::dialogs`): Solid, Camera,
|
||||
/// Light and Composition Settings keep what OK would apply here.
|
||||
pub fn dialog_state(&self) -> &panels::dialogs::DialogState {
|
||||
&self.dialog_state
|
||||
}
|
||||
|
||||
/// The open settings dialog's draft, to edit (tests and automation; the dialog's own fields
|
||||
/// do the same).
|
||||
pub fn dialog_state_mut(&mut self) -> &mut panels::dialogs::DialogState {
|
||||
&mut self.dialog_state
|
||||
}
|
||||
|
||||
/// Feed this frame's share of the queued synthetic input (control channel `ui.click`,
|
||||
/// `ui.drag`…) into `raw_input`. Pointer events go one per frame so egui sees press → moves
|
||||
/// → release as a real gesture.
|
||||
@@ -1688,9 +1692,11 @@ impl EffectcraftApp {
|
||||
}
|
||||
let t = self.tokens;
|
||||
let full = ui.max_rect();
|
||||
if let Some(d) = self.dialog {
|
||||
if self.dialog.is_some() {
|
||||
ui.disable();
|
||||
if d.leaves_window_undimmed() {
|
||||
// A dialog that edits what is behind it live (Preview) leaves the window at full
|
||||
// brightness: clicks still don't reach it, but the canvas is not greyed out.
|
||||
if self.dialog == Some(Dialog::GradientEditor) || panels::live_preview::active(self) {
|
||||
ui.set_opacity(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1800,6 +1800,8 @@ fn handle_shortcuts_impl(app: &mut EffectcraftApp, ctx: &egui::Context, tab_only
|
||||
if app.dialog == Some(crate::Dialog::UnsavedChanges) {
|
||||
app.dialog_state.unsaved.command = None;
|
||||
}
|
||||
// Escape is Cancel: a Preview that edited the project rolls back.
|
||||
crate::panels::live_preview::cancel(app);
|
||||
app.dialog = None;
|
||||
}
|
||||
return;
|
||||
@@ -1916,6 +1918,7 @@ fn handle_shortcuts_impl(app: &mut EffectcraftApp, ctx: &egui::Context, tab_only
|
||||
app.session.prefs = p;
|
||||
app.session.prefs_changed();
|
||||
}
|
||||
crate::panels::live_preview::cancel(app);
|
||||
app.dialog = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@ pub fn show(
|
||||
auto: &mut Registry,
|
||||
custom: &[CompPreset],
|
||||
cmds: &mut Vec<(&'static str, serde_json::Value)>,
|
||||
preview: Option<&mut bool>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut out = None;
|
||||
ui.horizontal(|ui| {
|
||||
@@ -470,6 +471,9 @@ pub fn show(
|
||||
if r.clicked() {
|
||||
out = Some(serde_json::Value::Null);
|
||||
}
|
||||
if let Some(on) = preview {
|
||||
super::live_preview::checkbox(ui, auto, on, "dialog.comp.preview");
|
||||
}
|
||||
});
|
||||
});
|
||||
out
|
||||
|
||||
@@ -65,6 +65,8 @@ pub struct DialogState {
|
||||
pub delete_items: super::delete_items::Pending,
|
||||
/// The Gradient Editor's draft and the property it edits.
|
||||
pub gradient: super::gradient_editor::Draft,
|
||||
/// Preview of the settings dialog being edited (Solid, Camera, Light, Composition Settings).
|
||||
pub live: super::live_preview::LivePreview,
|
||||
/// The user template the Delete Template prompt asks about: its id and name.
|
||||
pub delete_template: Option<(String, String)>,
|
||||
}
|
||||
@@ -75,6 +77,7 @@ pub fn open_new_comp(app: &mut EffectcraftApp) {
|
||||
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_state.live = Default::default();
|
||||
app.dialog = Some(Dialog::NewComp);
|
||||
}
|
||||
|
||||
@@ -107,6 +110,7 @@ pub fn open_comp_settings_for(app: &mut EffectcraftApp, cid: ItemId) -> Result<(
|
||||
app.dialog_state.editing_existing = true;
|
||||
app.dialog_state.editing_comp = Some(cid);
|
||||
app.dialog = Some(Dialog::CompSettings);
|
||||
super::live_preview::begin(app, Dialog::CompSettings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -121,6 +125,7 @@ pub fn open_new_solid(app: &mut EffectcraftApp) -> Result<(), String> {
|
||||
if d.solid_color == [0.0; 3] {
|
||||
d.solid_color = [0.2, 0.45, 0.85];
|
||||
}
|
||||
d.live = Default::default();
|
||||
app.dialog = Some(Dialog::SolidSettings);
|
||||
Ok(())
|
||||
}
|
||||
@@ -136,6 +141,7 @@ fn open_solid_settings(app: &mut EffectcraftApp, layer: u64, item: ItemId) -> Re
|
||||
(d.solid_name, d.solid_color, d.solid_size, d.solid_pixel_aspect) = (name, so.color, [so.width, so.height], so.pixel_aspect);
|
||||
(d.solid_layer, d.solid_shared, d.solid_affect_all) = (Some(layer), users > 1, false);
|
||||
app.dialog = Some(Dialog::SolidSettings);
|
||||
super::live_preview::begin(app, Dialog::SolidSettings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -179,7 +185,7 @@ pub(crate) fn modal_vertical(ctx: &egui::Context, title: &str, size: egui::Vec2,
|
||||
/// Where a modal dialog opens and whether it dims the window behind it. The default is the middle
|
||||
/// of the window over a dimmed backdrop.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Placement {
|
||||
pub struct Placement {
|
||||
/// The opening position as a fraction of the room the dialog can move in, from the middle
|
||||
/// `(0, 0)` to the bottom right `(1, 1)`.
|
||||
pub start: egui::Vec2,
|
||||
@@ -412,9 +418,13 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
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, &custom, &mut cmds);
|
||||
let live = existing && super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
modal_placed(ctx, "Composition Settings", vec2(600.0, 470.0), t, place, |ui| {
|
||||
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds, live.then_some(&mut preview));
|
||||
});
|
||||
app.dialog_state.live.on = preview;
|
||||
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;
|
||||
@@ -432,11 +442,19 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
}
|
||||
app.dialog_state.comp = d;
|
||||
match result {
|
||||
Some(serde_json::Value::Null) => app.dialog = None,
|
||||
Some(serde_json::Value::Null) => {
|
||||
super::live_preview::cancel(app);
|
||||
app.dialog = None;
|
||||
}
|
||||
Some(mut params) => {
|
||||
if existing && let (Some(cid), Some(m)) = (app.dialog_state.editing_comp, params.as_object_mut()) {
|
||||
m.insert("comp".into(), json!(cid.0));
|
||||
}
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if existing && super::live_preview::keep(app) {
|
||||
app.dialog = None;
|
||||
return;
|
||||
}
|
||||
let r = if existing { app.session.execute("comp.settings", params) } else { app.session.execute("comp.new", params) };
|
||||
if !existing && r.is_ok() {
|
||||
app.dialog_state.last_new_comp = Some(app.dialog_state.comp.clone());
|
||||
@@ -446,7 +464,15 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
}
|
||||
app.dialog = None;
|
||||
}
|
||||
None => {}
|
||||
None => {
|
||||
if existing && let Some(cid) = app.dialog_state.editing_comp {
|
||||
let mut params = super::comp_settings::params(&app.dialog_state.comp);
|
||||
if let Some(m) = params.as_object_mut() {
|
||||
m.insert("comp".into(), json!(cid.0));
|
||||
}
|
||||
super::live_preview::sync(app, "comp.settings", params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,8 +520,11 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let comp_size = app.session.active_comp().map(|c| (c.width, c.height)).unwrap_or((1920, 1080));
|
||||
let lock_id = egui::Id::new("solid-lock-aspect");
|
||||
let mut lock = ctx.data(|m| m.get_temp::<bool>(lock_id)).unwrap_or(true);
|
||||
let live = super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
let auto = &mut app.auto;
|
||||
modal(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, |ui| {
|
||||
modal_placed(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, place, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tr("Name:"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(320.0));
|
||||
@@ -581,16 +610,28 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let r = ui.button(tr("Cancel"));
|
||||
auto.add("dialog.solid.cancel", r.rect, "Cancel");
|
||||
close = r.clicked();
|
||||
if live {
|
||||
super::live_preview::checkbox(ui, auto, &mut preview, "dialog.solid.preview");
|
||||
}
|
||||
});
|
||||
});
|
||||
ctx.data_mut(|m| m.insert_temp(lock_id, lock));
|
||||
let ds = &mut app.dialog_state;
|
||||
ds.live.on = preview;
|
||||
(ds.solid_name, ds.solid_color, ds.solid_size, ds.solid_pixel_aspect, ds.solid_affect_all) = (name, color, size, par, affect_all);
|
||||
let (command, params) = (solid_command(ds), solid_params(ds));
|
||||
if ok || ui_enter(ctx) {
|
||||
if let Err(e) = app.session.execute(solid_command(ds), solid_params(ds)) {
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if !super::live_preview::keep(app)
|
||||
&& let Err(e) = app.session.execute(command, params)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
} else if close {
|
||||
super::live_preview::cancel(app);
|
||||
} else {
|
||||
super::live_preview::sync(app, command, params);
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
|
||||
@@ -87,8 +87,14 @@ pub fn open_camera(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), S
|
||||
}
|
||||
}
|
||||
};
|
||||
let editing = d.layer.is_some();
|
||||
app.dialog_state.camera = d;
|
||||
app.dialog = Some(Dialog::CameraSettings);
|
||||
if editing {
|
||||
super::live_preview::begin(app, Dialog::CameraSettings);
|
||||
} else {
|
||||
app.dialog_state.live = Default::default();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,13 +143,19 @@ pub fn open_light(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), St
|
||||
}
|
||||
}
|
||||
};
|
||||
let editing = d.layer.is_some();
|
||||
app.dialog_state.light = d;
|
||||
app.dialog = Some(Dialog::LightSettings);
|
||||
if editing {
|
||||
super::live_preview::begin(app, Dialog::LightSettings);
|
||||
} else {
|
||||
app.dialog_state.live = Default::default();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (egui::Rect, egui::Rect) {
|
||||
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool, preview: Option<&mut bool>) -> (egui::Rect, egui::Rect, egui::Rect) {
|
||||
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
|
||||
r.0 = b.rect;
|
||||
@@ -155,6 +167,9 @@ fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (e
|
||||
if c.clicked() {
|
||||
*close = true;
|
||||
}
|
||||
if let Some(on) = preview {
|
||||
r.2 = ui.checkbox(on, tr("Preview")).rect;
|
||||
}
|
||||
});
|
||||
r
|
||||
}
|
||||
@@ -163,8 +178,11 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let w = comp_w(app);
|
||||
let mut d = app.dialog_state.camera.clone();
|
||||
let (mut ok, mut close) = (false, false);
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
super::dialogs::modal(ctx, "Camera Settings", vec2(520.0, 420.0), t, |ui| {
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
let live = super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
super::dialogs::modal_placed(ctx, "Camera Settings", vec2(520.0, 420.0), t, place, |ui| {
|
||||
egui::Grid::new("cam-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
|
||||
ui.label(tr("Type"));
|
||||
egui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| {
|
||||
@@ -217,46 +235,66 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
rects = buttons(ui, t, &mut ok, &mut close);
|
||||
rects = buttons(ui, t, &mut ok, &mut close, live.then_some(&mut preview));
|
||||
});
|
||||
app.dialog_state.live.on = preview;
|
||||
if d.lock_to_zoom {
|
||||
d.focus = d.zoom;
|
||||
}
|
||||
app.auto.add("dialog.camera.ok", rects.0, "OK");
|
||||
app.auto.add("dialog.camera.cancel", rects.1, "Cancel");
|
||||
if live {
|
||||
app.auto.add("dialog.camera.preview", rects.2, "Preview");
|
||||
}
|
||||
app.dialog_state.camera = d.clone();
|
||||
let (id, p) = camera_command(&d);
|
||||
if ok || super::dialogs::ui_enter(ctx) {
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"type": if d.two_node { "twoNode" } else { "oneNode" },
|
||||
"zoom": d.zoom,
|
||||
"dof": d.dof,
|
||||
"focusDistance": d.focus,
|
||||
"aperture": d.aperture,
|
||||
"blurLevel": d.blur,
|
||||
});
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.cameraSettings"
|
||||
}
|
||||
None => "layer.newCamera",
|
||||
};
|
||||
if let Err(e) = app.session.execute(id, p) {
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if !super::live_preview::keep(app)
|
||||
&& let Err(e) = app.session.execute(id, p)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
} else if close {
|
||||
super::live_preview::cancel(app);
|
||||
} else {
|
||||
super::live_preview::sync(app, id, p);
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The command Camera Settings' OK runs, and its parameters.
|
||||
fn camera_command(d: &CameraDraft) -> (&'static str, Value) {
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"type": if d.two_node { "twoNode" } else { "oneNode" },
|
||||
"zoom": d.zoom,
|
||||
"dof": d.dof,
|
||||
"focusDistance": d.focus,
|
||||
"aperture": d.aperture,
|
||||
"blurLevel": d.blur,
|
||||
});
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.cameraSettings"
|
||||
}
|
||||
None => "layer.newCamera",
|
||||
};
|
||||
(id, p)
|
||||
}
|
||||
|
||||
pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
let mut d = app.dialog_state.light.clone();
|
||||
let (mut ok, mut close) = (false, false);
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
super::dialogs::modal(ctx, "Light Settings", vec2(480.0, 470.0), t, |ui| {
|
||||
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
|
||||
let live = super::live_preview::active(app);
|
||||
let place = super::live_preview::place(app);
|
||||
let mut preview = app.dialog_state.live.on;
|
||||
super::dialogs::modal_placed(ctx, "Light Settings", vec2(480.0, 470.0), t, place, |ui| {
|
||||
egui::Grid::new("light-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
|
||||
ui.label(tr("Name"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
|
||||
@@ -314,48 +352,65 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
ui.add_space(14.0);
|
||||
ui.label(egui::RichText::new(tr("Note: Shadows are only cast from layers with Cast Shadows enabled.")).font(Tokens::ui(11.0)).color(t.text_dim));
|
||||
ui.add_space(8.0);
|
||||
rects = buttons(ui, t, &mut ok, &mut close);
|
||||
rects = buttons(ui, t, &mut ok, &mut close, live.then_some(&mut preview));
|
||||
});
|
||||
app.dialog_state.live.on = preview;
|
||||
app.auto.add("dialog.light.ok", rects.0, "OK");
|
||||
app.auto.add("dialog.light.cancel", rects.1, "Cancel");
|
||||
if live {
|
||||
app.auto.add("dialog.light.preview", rects.2, "Preview");
|
||||
}
|
||||
app.dialog_state.light = d.clone();
|
||||
let (id, p) = light_command(&d);
|
||||
if ok || super::dialogs::ui_enter(ctx) {
|
||||
let kind = KINDS[d.kind.min(3)];
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"kind": kind.label(),
|
||||
"color": [d.color[0], d.color[1], d.color[2]],
|
||||
"intensity": d.intensity,
|
||||
});
|
||||
if kind == LightKind::Spot {
|
||||
p["coneAngle"] = json!(d.cone_angle);
|
||||
p["coneFeather"] = json!(d.cone_feather);
|
||||
}
|
||||
if kind != LightKind::Ambient {
|
||||
p["falloff"] = json!(d.falloff);
|
||||
p["radius"] = json!(d.radius);
|
||||
p["falloffDistance"] = json!(d.falloff_distance);
|
||||
p["castsShadows"] = json!(d.casts_shadows);
|
||||
p["shadowDarkness"] = json!(d.shadow_darkness);
|
||||
p["shadowDiffusion"] = json!(d.shadow_diffusion);
|
||||
}
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.lightSettings"
|
||||
}
|
||||
None => "layer.newLight",
|
||||
};
|
||||
if let Err(e) = app.session.execute(id, p) {
|
||||
// A preview already made the edit (one undo step): OK keeps it.
|
||||
if !super::live_preview::keep(app)
|
||||
&& let Err(e) = app.session.execute(id, p)
|
||||
{
|
||||
app.ui.status = e.to_string();
|
||||
}
|
||||
close = true;
|
||||
} else if close {
|
||||
super::live_preview::cancel(app);
|
||||
} else {
|
||||
super::live_preview::sync(app, id, p);
|
||||
}
|
||||
if close {
|
||||
app.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The command Light Settings' OK runs, and its parameters.
|
||||
fn light_command(d: &LightDraft) -> (&'static str, Value) {
|
||||
let kind = KINDS[d.kind.min(3)];
|
||||
let mut p = json!({
|
||||
"name": d.name,
|
||||
"kind": kind.label(),
|
||||
"color": [d.color[0], d.color[1], d.color[2]],
|
||||
"intensity": d.intensity,
|
||||
});
|
||||
if kind == LightKind::Spot {
|
||||
p["coneAngle"] = json!(d.cone_angle);
|
||||
p["coneFeather"] = json!(d.cone_feather);
|
||||
}
|
||||
if kind != LightKind::Ambient {
|
||||
p["falloff"] = json!(d.falloff);
|
||||
p["radius"] = json!(d.radius);
|
||||
p["falloffDistance"] = json!(d.falloff_distance);
|
||||
p["castsShadows"] = json!(d.casts_shadows);
|
||||
p["shadowDarkness"] = json!(d.shadow_darkness);
|
||||
p["shadowDiffusion"] = json!(d.shadow_diffusion);
|
||||
}
|
||||
let id = match d.layer {
|
||||
Some(l) => {
|
||||
p["layer"] = json!(l);
|
||||
"layer.lightSettings"
|
||||
}
|
||||
None => "layer.newLight",
|
||||
};
|
||||
(id, p)
|
||||
}
|
||||
|
||||
/// Menu/shortcut routing: commands that open a 3D settings dialog when invoked without params.
|
||||
/// Returns true when a dialog was opened.
|
||||
pub fn route(app: &mut EffectcraftApp, id: &str, params: &Value) -> Result<bool, String> {
|
||||
|
||||
@@ -315,8 +315,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
|
||||
}
|
||||
let mut answer: Option<bool> = None;
|
||||
// At the lower right, undimmed: the canvas it edits stays in view (After Effects).
|
||||
let place = super::dialogs::Placement { start: vec2(0.75, 0.8), dim: false };
|
||||
super::dialogs::modal_placed(ctx, tr("Gradient Editor"), vec2(520.0, 270.0), t, place, |ui| {
|
||||
super::dialogs::modal_placed(ctx, tr("Gradient Editor"), vec2(520.0, 270.0), t, super::live_preview::PLACE, |ui| {
|
||||
let w = ui.available_width().max(200.0);
|
||||
let (area, _) = ui.allocate_exact_size(vec2(w, 72.0), Sense::hover());
|
||||
let bar = Rect::from_min_max(pos2(area.min.x + 8.0, area.min.y + 22.0), pos2(area.max.x - 8.0, area.min.y + 48.0));
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Preview for the settings dialogs that edit something that already exists (Solid Settings on a
|
||||
//! solid layer, Camera Settings, Light Settings, Composition Settings): After Effects' Preview
|
||||
//! checkbox. Each change of the draft is applied to the project at once, so the canvas shows it
|
||||
//! while it is made; OK keeps it and Cancel and Escape take it back.
|
||||
//!
|
||||
//! Every step goes back to the checkpoint taken when the dialog opened (the project and its undo
|
||||
//! history) and then runs the dialog's command once with the draft's parameters. So the project is
|
||||
//! always "as it opened, plus one command": OK leaves exactly one undo step, whatever was tried
|
||||
//! meanwhile, and Cancel only has to put the checkpoint back. With Preview off nothing is applied
|
||||
//! until OK, and switching it off takes back what was applied.
|
||||
//!
|
||||
//! While a preview is active the dialog opens at the lower right of the window, without the dark
|
||||
//! backdrop, and the window behind it is not greyed out, so the canvas stays in view.
|
||||
|
||||
use egui::vec2;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::panels::dialogs::Placement;
|
||||
use crate::panels::layer_styles_dialog::{Checkpoint, checkpoint, restore};
|
||||
use crate::{Dialog, EffectcraftApp};
|
||||
|
||||
/// Where a previewing dialog opens: the lower right, undimmed.
|
||||
pub const PLACE: Placement = Placement { start: vec2(0.75, 0.8), dim: false };
|
||||
|
||||
/// The state of the one dialog being previewed (only one dialog is open at a time).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LivePreview {
|
||||
/// The dialog this belongs to; a preview left over from another one is ignored.
|
||||
dialog: Option<Dialog>,
|
||||
/// The Preview checkbox.
|
||||
pub on: bool,
|
||||
checkpoint: Option<Checkpoint>,
|
||||
/// The draft's parameters when the dialog opened: nothing is applied while it equals them.
|
||||
baseline: Option<Value>,
|
||||
/// The command and parameters the preview last applied.
|
||||
applied: Option<(&'static str, Value)>,
|
||||
}
|
||||
|
||||
impl LivePreview {
|
||||
fn active_for(&self, dialog: Option<Dialog>) -> bool {
|
||||
self.checkpoint.is_some() && self.dialog.is_some() && self.dialog == dialog
|
||||
}
|
||||
}
|
||||
|
||||
/// Start previewing for `dialog`, which has just opened on an existing item (Preview on).
|
||||
pub fn begin(app: &mut EffectcraftApp, dialog: Dialog) {
|
||||
app.dialog_state.live = LivePreview { dialog: Some(dialog), on: true, checkpoint: Some(checkpoint(app)), ..Default::default() };
|
||||
}
|
||||
|
||||
/// The dialog that is open is previewing.
|
||||
pub fn active(app: &EffectcraftApp) -> bool {
|
||||
app.dialog_state.live.active_for(app.dialog)
|
||||
}
|
||||
|
||||
/// Where the open dialog goes: the lower right, undimmed, while it previews; else the middle.
|
||||
pub fn place(app: &EffectcraftApp) -> Placement {
|
||||
if active(app) { PLACE } else { Placement::default() }
|
||||
}
|
||||
|
||||
/// Keep the project in step with the draft: `command` with `params` is what OK would run. The
|
||||
/// first call (the draft as it opened) only notes it.
|
||||
pub fn sync(app: &mut EffectcraftApp, command: &'static str, params: Value) {
|
||||
if !active(app) {
|
||||
return;
|
||||
}
|
||||
let live = &app.dialog_state.live;
|
||||
if live.baseline.is_none() {
|
||||
app.dialog_state.live.baseline = Some(params);
|
||||
return;
|
||||
}
|
||||
let want = live.on && live.baseline.as_ref() != Some(¶ms);
|
||||
let is_applied = live.applied.is_some();
|
||||
if want {
|
||||
if live.applied.as_ref().is_some_and(|(c, p)| *c == command && *p == params) {
|
||||
return;
|
||||
}
|
||||
if is_applied {
|
||||
put_back(app);
|
||||
}
|
||||
match app.session.execute(command, params.clone()) {
|
||||
Ok(_) => app.dialog_state.live.applied = Some((command, params)),
|
||||
Err(e) => app.ui.status = e.to_string(),
|
||||
}
|
||||
} else if is_applied {
|
||||
put_back(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// The project as the dialog found it.
|
||||
fn put_back(app: &mut EffectcraftApp) {
|
||||
if let Some(cp) = app.dialog_state.live.checkpoint.clone() {
|
||||
restore(app, cp);
|
||||
}
|
||||
app.dialog_state.live.applied = None;
|
||||
}
|
||||
|
||||
/// OK: the preview's result is the edit. Returns whether it already is (the caller then must not
|
||||
/// run the command again); otherwise the caller runs it, as without a preview.
|
||||
pub fn keep(app: &mut EffectcraftApp) -> bool {
|
||||
let kept = active(app) && app.dialog_state.live.applied.is_some();
|
||||
app.dialog_state.live = LivePreview::default();
|
||||
kept
|
||||
}
|
||||
|
||||
/// Cancel and Escape: take back whatever the preview applied.
|
||||
pub fn cancel(app: &mut EffectcraftApp) {
|
||||
if active(app) {
|
||||
put_back(app);
|
||||
}
|
||||
app.dialog_state.live = LivePreview::default();
|
||||
}
|
||||
|
||||
/// The Preview checkbox, when the open dialog previews; `id` is its automation id.
|
||||
pub fn checkbox(ui: &mut egui::Ui, auto: &mut crate::automation::Registry, on: &mut bool, id: &str) {
|
||||
let r = ui.checkbox(on, crate::i18n::tr("Preview"));
|
||||
auto.add(id, r.rect, "Preview");
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub mod key_dialogs;
|
||||
pub mod layer_panel;
|
||||
pub mod layer_styles_dialog;
|
||||
pub mod learn;
|
||||
pub mod live_preview;
|
||||
pub mod markers_ui;
|
||||
pub mod media_panels;
|
||||
pub mod misc;
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Preview in the settings dialogs that edit something that exists (After Effects' Preview
|
||||
//! checkbox): Solid Settings on a solid layer, Camera Settings, Light Settings and Composition
|
||||
//! Settings. A change of the draft reaches the project at once, so the canvas shows it while it is
|
||||
//! made; OK keeps it in one undo step, Cancel and Escape take it back, and with Preview off nothing
|
||||
//! is applied until OK. The window behind a previewing dialog is not dimmed, and the dialog opens
|
||||
//! at the lower right; the same dialogs creating something new keep the middle.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::{ItemKind, LayerId};
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp, menus};
|
||||
use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn harness(setup: impl FnOnce(&mut Session)) -> Harness<'static, EffectcraftApp> {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "duration": 10})).unwrap();
|
||||
setup(&mut s);
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
fn open(h: &mut Harness<'_, EffectcraftApp>, id: &str, params: Value) {
|
||||
let ctx = h.ctx.clone();
|
||||
menus::invoke(h.state_mut(), &ctx, id, params).unwrap();
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn center(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 {
|
||||
let r = h.state().auto.find(id).unwrap_or_else(|| panic!("no automation id {id}")).rect;
|
||||
pos2(r[0] + r[2] / 2.0, r[1] + r[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.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn escape(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
h.input_mut().events.push(Event::Key { key: Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.input_mut().events.push(Event::Key { key: Key::Escape, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn undo_len(h: &Harness<'_, EffectcraftApp>) -> usize {
|
||||
h.state().session.history.undo.len()
|
||||
}
|
||||
|
||||
fn solid_size(h: &Harness<'_, EffectcraftApp>, layer: u64) -> [u32; 2] {
|
||||
let s = &h.state().session;
|
||||
let l = s.active_comp().unwrap().layer(LayerId(layer)).unwrap();
|
||||
let effectcraft_engine::project::LayerSource::Solid { item } = l.source else { panic!("not a solid") };
|
||||
match &s.project.item(item).unwrap().kind {
|
||||
ItemKind::Solid(so) => [so.width, so.height],
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar(h: &Harness<'_, EffectcraftApp>, layer: u64, path: &str) -> f64 {
|
||||
let s = &h.state().session;
|
||||
let l = s.active_comp().unwrap().layer(LayerId(layer)).unwrap();
|
||||
l.props.prop(path).unwrap().value_at(l.layer_time(s.time())).as_f64()
|
||||
}
|
||||
|
||||
fn comp_width(h: &Harness<'_, EffectcraftApp>) -> u32 {
|
||||
h.state().session.active_comp().unwrap().width
|
||||
}
|
||||
|
||||
fn solid_setup(s: &mut Session) {
|
||||
s.execute("layer.newSolid", json!({"name": "Box", "color": "#ff0000", "width": 100, "height": 100})).unwrap();
|
||||
}
|
||||
|
||||
fn solid_layer(h: &Harness<'_, EffectcraftApp>) -> u64 {
|
||||
h.state().session.active_comp().unwrap().layers[0].id.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_settings_previews_live_and_cancel_and_escape_take_it_back() {
|
||||
let mut h = harness(solid_setup);
|
||||
let l = solid_layer(&h);
|
||||
let undo = undo_len(&h);
|
||||
open(&mut h, "layer.settings", json!({"layer": l}));
|
||||
assert_eq!(h.state().dialog, Some(Dialog::SolidSettings));
|
||||
assert!(h.state().auto.find("dialog.solid.preview").is_some(), "a Preview checkbox");
|
||||
h.state_mut().dialog_state_mut().solid_size = [200, 150];
|
||||
h.run_steps(3);
|
||||
assert_eq!(solid_size(&h, l), [200, 150], "the canvas shows the new size before OK");
|
||||
h.state_mut().dialog_state_mut().solid_size = [240, 120];
|
||||
h.run_steps(3);
|
||||
assert_eq!(solid_size(&h, l), [240, 120]);
|
||||
assert_eq!(undo_len(&h), undo + 1, "however much was tried, the project is the original plus one command");
|
||||
click(&mut h, "dialog.solid.cancel");
|
||||
assert_eq!(h.state().dialog, None);
|
||||
assert_eq!(solid_size(&h, l), [100, 100], "Cancel puts it back");
|
||||
assert_eq!(undo_len(&h), undo, "and leaves no undo step");
|
||||
// Escape does the same.
|
||||
open(&mut h, "layer.settings", json!({"layer": l}));
|
||||
h.state_mut().dialog_state_mut().solid_size = [200, 150];
|
||||
h.run_steps(3);
|
||||
assert_eq!(solid_size(&h, l), [200, 150]);
|
||||
escape(&mut h);
|
||||
assert_eq!(h.state().dialog, None);
|
||||
assert_eq!(solid_size(&h, l), [100, 100]);
|
||||
assert_eq!(undo_len(&h), undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_settings_ok_keeps_the_preview_as_one_undo_step() {
|
||||
let mut h = harness(solid_setup);
|
||||
let l = solid_layer(&h);
|
||||
let undo = undo_len(&h);
|
||||
open(&mut h, "layer.settings", json!({"layer": l}));
|
||||
h.state_mut().dialog_state_mut().solid_size = [200, 150];
|
||||
h.run_steps(3);
|
||||
h.state_mut().dialog_state_mut().solid_size = [220, 130];
|
||||
h.run_steps(3);
|
||||
click(&mut h, "dialog.solid.ok");
|
||||
assert_eq!(h.state().dialog, None);
|
||||
assert_eq!(solid_size(&h, l), [220, 130]);
|
||||
assert_eq!(undo_len(&h), undo + 1, "one undo step");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(solid_size(&h, l), [100, 100], "undone in one go");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_preview_off_nothing_is_applied_until_ok() {
|
||||
let mut h = harness(solid_setup);
|
||||
let l = solid_layer(&h);
|
||||
let undo = undo_len(&h);
|
||||
open(&mut h, "layer.settings", json!({"layer": l}));
|
||||
h.state_mut().dialog_state_mut().solid_size = [200, 150];
|
||||
h.run_steps(3);
|
||||
assert_eq!(solid_size(&h, l), [200, 150]);
|
||||
click(&mut h, "dialog.solid.preview");
|
||||
assert!(!h.state().dialog_state().live.on);
|
||||
assert_eq!(solid_size(&h, l), [100, 100], "switching Preview off takes the preview back");
|
||||
assert_eq!(undo_len(&h), undo);
|
||||
h.state_mut().dialog_state_mut().solid_size = [260, 140];
|
||||
h.run_steps(3);
|
||||
assert_eq!(solid_size(&h, l), [100, 100], "nothing is applied while editing");
|
||||
click(&mut h, "dialog.solid.ok");
|
||||
assert_eq!(solid_size(&h, l), [260, 140], "OK applies the draft");
|
||||
assert_eq!(undo_len(&h), undo + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_settings_preview() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("layer.newCamera", json!({})).unwrap();
|
||||
});
|
||||
let l = solid_layer(&h);
|
||||
let before = scalar(&h, l, "cameraOptions/zoom");
|
||||
let undo = undo_len(&h);
|
||||
open(&mut h, "layer.cameraSettings", json!({"layer": l}));
|
||||
assert_eq!(h.state().dialog, Some(Dialog::CameraSettings));
|
||||
assert!(h.state().auto.find("dialog.camera.preview").is_some());
|
||||
h.state_mut().dialog_state_mut().camera.zoom = before + 500.0;
|
||||
h.run_steps(3);
|
||||
assert!((scalar(&h, l, "cameraOptions/zoom") - (before + 500.0)).abs() < 1e-6, "the zoom changes live");
|
||||
assert_eq!(undo_len(&h), undo + 1);
|
||||
click(&mut h, "dialog.camera.cancel");
|
||||
assert!((scalar(&h, l, "cameraOptions/zoom") - before).abs() < 1e-6, "Cancel puts the zoom back");
|
||||
assert_eq!(undo_len(&h), undo);
|
||||
open(&mut h, "layer.cameraSettings", json!({"layer": l}));
|
||||
h.state_mut().dialog_state_mut().camera.zoom = before + 300.0;
|
||||
h.run_steps(3);
|
||||
click(&mut h, "dialog.camera.ok");
|
||||
assert!((scalar(&h, l, "cameraOptions/zoom") - (before + 300.0)).abs() < 1e-6, "OK keeps it");
|
||||
assert_eq!(undo_len(&h), undo + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_settings_preview() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("layer.newLight", json!({"kind": "Point"})).unwrap();
|
||||
});
|
||||
let l = solid_layer(&h);
|
||||
let before = scalar(&h, l, "lightOptions/intensity");
|
||||
let undo = undo_len(&h);
|
||||
open(&mut h, "layer.lightSettings", json!({"layer": l}));
|
||||
assert_eq!(h.state().dialog, Some(Dialog::LightSettings));
|
||||
assert!(h.state().auto.find("dialog.light.preview").is_some());
|
||||
h.state_mut().dialog_state_mut().light.intensity = before + 37.0;
|
||||
h.run_steps(3);
|
||||
assert!((scalar(&h, l, "lightOptions/intensity") - (before + 37.0)).abs() < 1e-6, "the intensity changes live");
|
||||
escape(&mut h);
|
||||
assert_eq!(h.state().dialog, None);
|
||||
assert!((scalar(&h, l, "lightOptions/intensity") - before).abs() < 1e-6, "Escape puts it back");
|
||||
assert_eq!(undo_len(&h), undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_settings_preview() {
|
||||
let mut h = harness(|_| {});
|
||||
let before = comp_width(&h);
|
||||
let undo = undo_len(&h);
|
||||
open(&mut h, "comp.settings", json!({}));
|
||||
assert_eq!(h.state().dialog, Some(Dialog::CompSettings));
|
||||
assert!(h.state().auto.find("dialog.comp.preview").is_some());
|
||||
h.state_mut().dialog_state_mut().comp.width = before + 80;
|
||||
h.run_steps(3);
|
||||
assert_eq!(comp_width(&h), before + 80, "the comp is resized live");
|
||||
click(&mut h, "dialog.comp.cancel");
|
||||
assert_eq!(comp_width(&h), before, "Cancel puts the size back");
|
||||
assert_eq!(undo_len(&h), undo);
|
||||
open(&mut h, "comp.settings", json!({}));
|
||||
h.state_mut().dialog_state_mut().comp.width = before + 40;
|
||||
h.run_steps(3);
|
||||
click(&mut h, "dialog.comp.ok");
|
||||
assert_eq!(comp_width(&h), before + 40, "OK keeps it");
|
||||
assert_eq!(undo_len(&h), undo + 1, "in one undo step");
|
||||
}
|
||||
|
||||
/// A previewing dialog opens at the lower right so the canvas stays in view; the same dialogs
|
||||
/// creating something new keep the middle and have no Preview.
|
||||
#[test]
|
||||
fn previewing_dialogs_open_at_the_lower_right_and_new_ones_in_the_middle() {
|
||||
let mut h = harness(solid_setup);
|
||||
let l = solid_layer(&h);
|
||||
open(&mut h, "layer.settings", json!({"layer": l}));
|
||||
let ok = center(&h, "dialog.solid.ok");
|
||||
assert!(ok.x > 800.0 && ok.y > 500.0, "Solid Settings on a layer is in the lower right: {ok:?}");
|
||||
click(&mut h, "dialog.solid.cancel");
|
||||
open(&mut h, "layer.newSolid", json!({}));
|
||||
assert_eq!(h.state().dialog, Some(Dialog::SolidSettings));
|
||||
assert!(h.state().auto.find("dialog.solid.preview").is_none(), "a new solid has no Preview");
|
||||
let ok = center(&h, "dialog.solid.ok");
|
||||
assert!(ok.x < 1000.0, "New Solid stays in the middle: {ok:?}");
|
||||
}
|
||||
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=6 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 6.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// A previewing dialog opens at the lower right, but it can be dragged by its title to wherever
|
||||
/// the canvas needs to be seen, and the preview keeps working from there.
|
||||
#[test]
|
||||
fn a_previewing_dialog_can_be_dragged_by_its_title() {
|
||||
let mut h = harness(solid_setup);
|
||||
let l = solid_layer(&h);
|
||||
open(&mut h, "layer.settings", json!({"layer": l}));
|
||||
let ok = center(&h, "dialog.solid.ok");
|
||||
let title = h.get_by_label("Solid Settings").rect().center();
|
||||
drag(&mut h, title, title + egui::vec2(-500.0, -300.0));
|
||||
let moved = center(&h, "dialog.solid.ok");
|
||||
assert!(moved.x < ok.x - 300.0 && moved.y < ok.y - 150.0, "the dialog followed the drag: {ok:?} -> {moved:?}");
|
||||
// Still previewing, and the position is kept from frame to frame.
|
||||
h.state_mut().dialog_state_mut().solid_size = [200, 150];
|
||||
h.run_steps(3);
|
||||
assert_eq!(solid_size(&h, l), [200, 150]);
|
||||
assert_eq!(center(&h, "dialog.solid.ok"), moved, "it stays where it was put");
|
||||
}
|
||||
@@ -571,12 +571,3 @@ fn the_escape_after_the_picker_closes_cancels_the_editor() {
|
||||
assert_eq!(h.state().dialog, None, "the next pass's Escape cancels the editor");
|
||||
assert_eq!(h.state().session.project, project, "and changes nothing");
|
||||
}
|
||||
|
||||
/// The editor shows its edits on the canvas live, so the window behind it is not greyed out while
|
||||
/// it is open; other dialogs still are.
|
||||
#[test]
|
||||
fn only_dialogs_that_preview_leave_the_window_undimmed() {
|
||||
assert!(Dialog::GradientEditor.leaves_window_undimmed());
|
||||
assert!(!Dialog::SolidSettings.leaves_window_undimmed());
|
||||
assert!(!Dialog::KeyVelocity.leaves_window_undimmed());
|
||||
}
|
||||
@@ -274,12 +274,13 @@ fn comp_context_menu_opens_that_comps_settings() {
|
||||
assert_eq!(h.state().dialog, Some(Dialog::CompSettings));
|
||||
assert_eq!(h.state().session.active_comp_id(), Some(main), "the viewer keeps its comp");
|
||||
// A new name, then OK: Side's own size goes back to Side under it. Main's values in the
|
||||
// dialog, or OK writing to the viewer's comp, would resize or rename the wrong one.
|
||||
// dialog, or OK writing to the viewer's comp, would resize or rename the wrong one. The
|
||||
// dialog previews the name as it is typed; OK keeps that as one undo step.
|
||||
let undo = h.state().session.history.undo.len();
|
||||
click(&mut h, "dialog.comp.name");
|
||||
h.input_mut().events.push(Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND });
|
||||
h.input_mut().events.push(Event::Text("Side 2".into()));
|
||||
h.run_steps(2);
|
||||
let undo = h.state().session.history.undo.len();
|
||||
h.input_mut().events.push(Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().dialog, None, "OK closed the dialog");
|
||||
|
||||
Reference in new issue
Block a user