ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
399 lines
14 KiB
Rust
399 lines
14 KiB
Rust
//! Transitions, effects and their previews from the deck (C10.4).
|
|
|
|
use ely_lok::show::{Part, Transition};
|
|
use ely_office::prelude::*;
|
|
use gpui::{AppContext as _, TestAppContext};
|
|
|
|
use super::objects::{act, blank};
|
|
use super::{settle, update};
|
|
use crate::preview::Playing;
|
|
|
|
#[test]
|
|
fn a_transition_and_an_effect_land_and_preview() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let motion = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.and_then(|tab| tab.extra.motion.clone())
|
|
};
|
|
settle(&cx, |app| motion(app).is_some());
|
|
let fade = Transition {
|
|
preset: "fade-smoothly".into(),
|
|
duration: 0.5,
|
|
auto: false,
|
|
time: 0.0,
|
|
};
|
|
let json = ely_lok::show::transition_json(&fade, false);
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.design_command("slide.transition", &[&json], cx)
|
|
});
|
|
settle(&cx, |app| motion(app).is_some_and(|(t, _)| t == fade));
|
|
act(
|
|
&mut cx,
|
|
&deck,
|
|
("draw.shape", &["rectangle", "true"]),
|
|
|o| o.selection.is_some(),
|
|
);
|
|
let add = serde_json::json!({ "preset": "ooo-entrance-fly-in" }).to_string();
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.design_command("animation.add", &[&add], cx)
|
|
});
|
|
settle(&cx, |app| {
|
|
motion(app).is_some_and(|(_, effects)| effects.len() == 1)
|
|
});
|
|
let hash = cx.update(|app| motion(app).expect("read").1[0].hash.clone());
|
|
update(&mut cx, &deck, |deck, cx| deck.preview_effects(cx));
|
|
settle(&cx, |app| deck.read(app).preview.is_some());
|
|
let moving =
|
|
cx.update(
|
|
|app| match &deck.read(app).preview.as_ref().expect("playing").playing {
|
|
Playing::Effects { layers, .. } => layers
|
|
.iter()
|
|
.filter(|(part, _)| matches!(part, Part::Moving { hash: h, .. } if *h == hash))
|
|
.count(),
|
|
Playing::Transition { .. } => 0,
|
|
},
|
|
);
|
|
assert_eq!(moving, 1, "the shape plays as its own layer");
|
|
update(&mut cx, &deck, |deck, cx| deck.stop_preview(cx));
|
|
update(&mut cx, &deck, |deck, cx| deck.preview_transition(cx));
|
|
settle(&cx, |app| deck.read(app).preview.is_some());
|
|
let first = cx.update(
|
|
|app| match &deck.read(app).preview.as_ref().expect("playing").playing {
|
|
Playing::Transition { from, preset, .. } => (from.is_none(), preset.clone()),
|
|
Playing::Effects { .. } => (false, String::new()),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
first,
|
|
(true, "fade-smoothly".to_string()),
|
|
"the first slide comes from black"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn inserted_media_reads_back_and_is_picked() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let clip = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../tests/fixtures/media/clip.mp4");
|
|
let url = ely_lok::file_url(&clip);
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
let tab = deck.core.active;
|
|
deck.run_in(tab, "media.insert", &[&url, "false", "8000", "4500"], cx);
|
|
deck.force_reread(tab.expect("a tab"), cx);
|
|
});
|
|
let media = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.map(|tab| tab.extra.media.clone())
|
|
.unwrap_or_default()
|
|
};
|
|
settle(&cx, |app| media(app).len() == 1);
|
|
let index = cx.update(|app| media(app)[0].index).to_string();
|
|
act(&mut cx, &deck, ("object.select", &[&index, "false"]), |o| {
|
|
o.selection.is_some()
|
|
});
|
|
let picked = cx.update(|app| deck.read(app).picked_media());
|
|
assert!(picked.is_some_and(|(_, media)| !media.missing && !media.link));
|
|
}
|
|
|
|
#[test]
|
|
fn a_stop_cancels_a_preview_on_its_way_and_a_slide_change_ends_one() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let ready = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.is_some_and(|tab| tab.extra.motion.is_some() && tab.extra.history.is_some())
|
|
};
|
|
settle(&cx, ready);
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.preview_transition(cx);
|
|
deck.stop_preview(cx);
|
|
});
|
|
for _ in 0..100 {
|
|
cx.executor()
|
|
.advance_clock(std::time::Duration::from_millis(20));
|
|
cx.run_until_parked();
|
|
}
|
|
assert!(
|
|
cx.update(|app| deck.read(app).preview.is_none()),
|
|
"the stopped read never plays"
|
|
);
|
|
update(&mut cx, &deck, |deck, cx| deck.preview_transition(cx));
|
|
settle(&cx, |app| deck.read(app).preview.is_some());
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
let tab = deck.core.active;
|
|
deck.run_in(tab, "slide.insert", &[], cx);
|
|
deck.force_reread(tab.expect("a tab"), cx);
|
|
});
|
|
settle(&cx, |app| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.is_some_and(|tab| tab.extra.slides.current == 1)
|
|
});
|
|
update(&mut cx, &deck, |deck, cx| deck.drop_stale_preview(cx));
|
|
assert!(
|
|
cx.update(|app| deck.read(app).preview.is_none()),
|
|
"another slide ends it"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn typing_ends_a_preview_before_any_undo_step() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
act(&mut cx, &deck, ("object.select", &["0", "false"]), |o| {
|
|
o.selection.is_some()
|
|
});
|
|
let view = cx.update(|app| deck.read(app).active_tab().expect("a tab").view.clone());
|
|
let typed = |cx: &mut TestAppContext, text: &str| {
|
|
let before = cx.update(|app| view.read(app).edited());
|
|
cx.update(|app| view.update(app, |view, _| view.insert(text)));
|
|
settle(cx, |app| {
|
|
view.read(app).edited() > before && view.read(app).settled()
|
|
});
|
|
};
|
|
typed(&mut cx, "A");
|
|
let ready = |app: &gpui::App| {
|
|
let tab = deck.read(app).active_tab();
|
|
tab.is_some_and(|tab| tab.extra.motion.is_some() && tab.extra.history.is_some())
|
|
};
|
|
settle(&cx, ready);
|
|
update(&mut cx, &deck, |deck, cx| deck.preview_transition(cx));
|
|
settle(&cx, |app| deck.read(app).preview.is_some());
|
|
let undo = |app: &mut gpui::App| deck.read(app).active_tab()?.extra.history.clone();
|
|
let steps = cx.update(undo);
|
|
typed(&mut cx, "B");
|
|
assert_eq!(cx.update(undo), steps, "no undo step yet");
|
|
update(&mut cx, &deck, |deck, cx| deck.drop_stale_preview(cx));
|
|
assert!(
|
|
cx.update(|app| deck.read(app).preview.is_none()),
|
|
"the typed text ends it"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn enter_then_blur_sends_one_duration() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let motion = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.and_then(|tab| tab.extra.motion.clone())
|
|
};
|
|
settle(&cx, |app| motion(app).is_some());
|
|
let panel = cx.update(|app| {
|
|
let weak = deck.downgrade();
|
|
app.new(|cx| crate::design::Panel::new(weak, cx))
|
|
});
|
|
let now = cx.update(|app| motion(app).expect("read").0);
|
|
let send = crate::motion::dedupe(panel, "transition.duration", now.duration, |deck, n, cx| {
|
|
deck.send_transition(|t| t.duration = n, false, cx)
|
|
});
|
|
let steps = super::undo_steps(&mut cx, &deck);
|
|
let handle = cx
|
|
.update(|app| deck.read(app).core.window)
|
|
.expect("a window");
|
|
for _ in 0..2 {
|
|
cx.update_window(handle, |_, window, app| send(3.5, window, app))
|
|
.expect("the window lives");
|
|
}
|
|
settle(&cx, |app| {
|
|
motion(app).is_some_and(|(t, _)| t.duration == 3.5)
|
|
});
|
|
assert_eq!(
|
|
super::undo_steps(&mut cx, &deck),
|
|
steps + 1,
|
|
"one step for Enter and blur"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn two_fields_sent_before_a_read_both_land() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let motion = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.and_then(|tab| tab.extra.motion.clone())
|
|
};
|
|
settle(&cx, |app| motion(app).is_some());
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_transition(
|
|
|t| (t.auto, t.duration, t.time) = (true, 2.0, 4.0),
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
settle(&cx, |app| {
|
|
motion(app).is_some_and(|(t, _)| t.auto && t.time == 4.0)
|
|
});
|
|
let panel = cx.update(|app| {
|
|
let weak = deck.downgrade();
|
|
app.new(|cx| crate::design::Panel::new(weak, cx))
|
|
});
|
|
let duration =
|
|
crate::motion::dedupe(panel.clone(), "transition.duration", 2.0, |deck, n, cx| {
|
|
deck.send_transition(|t| t.duration = n, false, cx)
|
|
});
|
|
let time = crate::motion::dedupe(panel, "transition.time", 4.0, |deck, n, cx| {
|
|
deck.send_transition(|t| t.time = n, false, cx)
|
|
});
|
|
let handle = cx
|
|
.update(|app| deck.read(app).core.window)
|
|
.expect("a window");
|
|
cx.update_window(handle, |_, window, app| {
|
|
duration(3.5, window, app);
|
|
time(7.0, window, app);
|
|
})
|
|
.expect("the window lives");
|
|
settle(&cx, |app| {
|
|
let asked = deck
|
|
.read(app)
|
|
.active_tab()
|
|
.map(|t| t.extra.transition_asked.is_none());
|
|
asked == Some(true) && motion(app).is_some_and(|(t, _)| t.time == 7.0)
|
|
});
|
|
let read = cx.update(|app| motion(app).expect("read").0);
|
|
assert_eq!((read.duration, read.time), (3.5, 7.0));
|
|
}
|
|
|
|
#[test]
|
|
fn an_undone_duration_is_not_sent_again() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let motion = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.and_then(|tab| tab.extra.motion.clone())
|
|
};
|
|
let asked = |app: &gpui::App| {
|
|
let tab = deck.read(app).active_tab();
|
|
tab.is_some_and(|tab| tab.extra.transition_asked.is_some())
|
|
};
|
|
settle(&cx, |app| motion(app).is_some());
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_transition(
|
|
|t| (t.auto, t.duration, t.time) = (true, 2.0, 4.0),
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
settle(&cx, |app| {
|
|
!asked(app) && motion(app).is_some_and(|(t, _)| t.time == 4.0)
|
|
});
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_transition(|t| t.duration = 3.5, false, cx);
|
|
let tab = deck.core.active;
|
|
deck.run_in(tab, "edit.undo", &[], cx);
|
|
deck.force_reread(tab.expect("a tab"), cx);
|
|
});
|
|
settle(&cx, |app| !asked(app));
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_transition(|t| t.time = 7.0, false, cx)
|
|
});
|
|
settle(&cx, |app| {
|
|
!asked(app) && motion(app).is_some_and(|(t, _)| t.time == 7.0)
|
|
});
|
|
let read = cx.update(|app| motion(app).expect("read").0);
|
|
assert_eq!((read.duration, read.time), (2.0, 7.0));
|
|
}
|
|
|
|
#[test]
|
|
fn two_effects_take_the_same_value_before_a_read() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut cx = TestAppContext::single();
|
|
let Some(deck) = blank(&mut cx, dir.path()) else {
|
|
eprintln!("SKIP: no LOK build");
|
|
return;
|
|
};
|
|
let effects = |app: &gpui::App| {
|
|
deck.read(app)
|
|
.active_tab()
|
|
.and_then(|tab| tab.extra.motion.clone())
|
|
.map(|(_, effects)| effects)
|
|
.unwrap_or_default()
|
|
};
|
|
act(
|
|
&mut cx,
|
|
&deck,
|
|
("draw.shape", &["rectangle", "true"]),
|
|
|o| o.selection.is_some(),
|
|
);
|
|
let add = serde_json::json!({ "preset": "ooo-entrance-fade-in" }).to_string();
|
|
for count in 1..=2 {
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.design_command("animation.add", &[&add], cx)
|
|
});
|
|
settle(&cx, |app| effects(app).len() == count);
|
|
}
|
|
for at in 0..2 {
|
|
update(&mut cx, &deck, |deck, cx| {
|
|
deck.send_effect(at, serde_json::json!({ "duration": 2.0 }), cx)
|
|
});
|
|
}
|
|
settle(&cx, |app| effects(app).iter().all(|e| e.duration == 2.0));
|
|
let panel = cx.update(|app| {
|
|
let weak = deck.downgrade();
|
|
app.new(|cx| crate::design::Panel::new(weak, cx))
|
|
});
|
|
let (slide, listed) = cx.update(|app| {
|
|
let tab = deck.read(app).active_tab().expect("a tab");
|
|
let slide = tab.extra.slides.list[tab.extra.slides.current].name.clone();
|
|
(slide, effects(app))
|
|
});
|
|
let handle = cx
|
|
.update(|app| deck.read(app).core.window)
|
|
.expect("a window");
|
|
let steps = super::undo_steps(&mut cx, &deck);
|
|
cx.update_window(handle, |_, window, app| {
|
|
for (at, effect) in listed.iter().enumerate() {
|
|
let key = crate::motion::effect_key("duration", &slide, (at, effect));
|
|
let send = crate::motion::dedupe(panel.clone(), key, 2.0, move |deck, n, cx| {
|
|
deck.send_effect(at, serde_json::json!({ "duration": n }), cx)
|
|
});
|
|
// Enter, then blur.
|
|
send(3.5, window, app);
|
|
send(3.5, window, app);
|
|
}
|
|
})
|
|
.expect("the window lives");
|
|
settle(&cx, |app| effects(app).iter().all(|e| e.duration == 3.5));
|
|
assert_eq!(
|
|
super::undo_steps(&mut cx, &deck),
|
|
steps + 2,
|
|
"one step each"
|
|
);
|
|
}
|