Merge PR #4 from storybored379
Control: screenshots and ui.elements wait for the UI to settle
This commit is contained in:
commit
0fb2ba99ca
|
|
@ -101,6 +101,10 @@ pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest)
|
|||
"ui.inspect" => ok(inspect(app, ctx)),
|
||||
"perf.stats" => ok(crate::perf::stats(app)),
|
||||
"ui.elements" => {
|
||||
if app.timeline_still < 2 {
|
||||
// rects of timeline elements are still moving (e.g. the fit after opening a sequence)
|
||||
return Outcome::Retry("the timeline is still zooming".into());
|
||||
}
|
||||
let prefix = s("prefix").unwrap_or("");
|
||||
ok(serde_json::to_value(app.auto.query(prefix)).unwrap_or_default())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ pub struct Playback {
|
|||
|
||||
/// How long `ui.screenshot` waits for the window to present the frame.
|
||||
const SCREENSHOT_TIMEOUT_S: f64 = 10.0;
|
||||
/// How long `ui.screenshot` waits for the UI to settle (monitors showing their exact frames, the
|
||||
/// timeline zoom animation finished) before capturing what is there.
|
||||
const SCREENSHOT_SETTLE_MAX_S: f64 = 5.0;
|
||||
|
||||
pub struct FilmcraftApp {
|
||||
pub session: Session,
|
||||
|
|
@ -155,6 +158,12 @@ pub struct FilmcraftApp {
|
|||
/// Screenshots waiting for their frame: (token, path, crop, reply, give-up time).
|
||||
pending_screenshots: Vec<(u64, Option<String>, Option<[f32; 4]>, Sender<Value>, f64)>,
|
||||
queued_screenshots: Vec<(u64, f64, u32)>,
|
||||
/// A paused monitor drew a stand-in (nearest cached) picture last frame: its exact frame is
|
||||
/// still decoding.
|
||||
pub(crate) monitor_inexact: bool,
|
||||
/// Consecutive frames the timeline zoom / scroll has been at rest. `ui.elements` answers from
|
||||
/// the frame before the last one, so its timeline rects are final from 2 on.
|
||||
pub(crate) timeline_still: u32,
|
||||
input_waiters: Vec<Sender<Value>>,
|
||||
next_token: u64,
|
||||
styled: bool,
|
||||
|
|
@ -336,6 +345,8 @@ impl FilmcraftApp {
|
|||
status_seen: (String::new(), 0.0),
|
||||
pending_screenshots: Vec::new(),
|
||||
queued_screenshots: Vec::new(),
|
||||
monitor_inexact: false,
|
||||
timeline_still: 0,
|
||||
input_waiters: Vec::new(),
|
||||
next_token: 1,
|
||||
styled: false,
|
||||
|
|
@ -929,13 +940,16 @@ impl FilmcraftApp {
|
|||
self.control_rx = Some(rx);
|
||||
}
|
||||
|
||||
fn issue_screenshots(&mut self, ctx: &egui::Context) {
|
||||
/// Capture once the UI shows the current state (`settled`): after a seek the monitor shows the
|
||||
/// nearest cached picture until the exact frame is decoded, and an agent must not be handed
|
||||
/// the stand-in.
|
||||
fn issue_screenshots(&mut self, ctx: &egui::Context, settled: bool) {
|
||||
let now = ctx.input(|i| i.time);
|
||||
let mut any = false;
|
||||
self.queued_screenshots.retain_mut(|(token, at, frames)| {
|
||||
*frames += 1;
|
||||
any = true;
|
||||
if now >= *at && *frames >= 3 {
|
||||
if now >= *at && *frames >= 3 && (settled || now >= *at + SCREENSHOT_SETTLE_MAX_S) {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new(*token)));
|
||||
false
|
||||
} else {
|
||||
|
|
@ -1234,6 +1248,7 @@ impl eframe::App for FilmcraftApp {
|
|||
// Nothing is shown while the window is hidden: not a dropped frame.
|
||||
self.playback.hidden = true;
|
||||
}
|
||||
self.timeline_still = if self.ui.timeline.animating() { 0 } else { self.timeline_still.saturating_add(1) };
|
||||
let had_synthetic = !self.synthetic.is_empty();
|
||||
self.drain_control(ctx);
|
||||
if !self.synthetic.is_empty() && !had_synthetic {
|
||||
|
|
@ -1248,7 +1263,8 @@ impl eframe::App for FilmcraftApp {
|
|||
let _ = w.send(json!({"ok": true, "result": null}));
|
||||
}
|
||||
}
|
||||
self.issue_screenshots(ctx);
|
||||
let settled = !std::mem::take(&mut self.monitor_inexact) && self.timeline_still > 0;
|
||||
self.issue_screenshots(ctx, settled);
|
||||
self.collect_screenshots(ctx);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -183,6 +183,9 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect, which: Which)
|
|||
} else {
|
||||
cpu_texture(app, &ctx, &tex_name, key, display)
|
||||
};
|
||||
if !playing && !exact {
|
||||
app.monitor_inexact = true;
|
||||
}
|
||||
if playing {
|
||||
if std::mem::take(&mut app.playback.hidden) {
|
||||
app.playback.meter.resync(frame, exact);
|
||||
|
|
|
|||
|
|
@ -200,6 +200,13 @@ pub struct TimelineView {
|
|||
pub track_lanes: std::collections::BTreeMap<u64, String>,
|
||||
}
|
||||
|
||||
impl TimelineView {
|
||||
/// The zoom / scroll animation is still running (element rects are moving).
|
||||
pub fn animating(&self) -> bool {
|
||||
self.pps != self.target_pps || self.scroll != self.target_scroll
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimelineView {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -313,3 +313,50 @@ fn snap_in_program_monitor_and_graphics_menu() {
|
|||
assert_eq!(shortcut, "Cmd+Shift+]");
|
||||
d.shot("graphics-menu");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elements_wait_for_the_timeline_zoom_to_settle() {
|
||||
let mut d = Driver::demo();
|
||||
let project = d.exec("project.inspect", json!({}));
|
||||
let item = first_media_item(&project).expect("a movie in the demo project");
|
||||
// opening a sequence fits the timeline with an animated zoom: rects read right away must
|
||||
// already be the final ones
|
||||
d.exec("file.newSequence", json!({"name": "Zoom", "fromItem": item}));
|
||||
let clip = d.exec("sequence.inspect", json!({}))["video"][0]["items"][0]["clip"].as_u64().expect("clip");
|
||||
let id = format!("timeline.clip.{clip}");
|
||||
let first = d.rect(&id);
|
||||
d.frames(120);
|
||||
let settled = d.rect(&id);
|
||||
for k in 0..4 {
|
||||
assert!((first[k] - settled[k]).abs() < 0.5, "rect read while zooming {first:?}, settled {settled:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Frames until `ui.elements` answers (it retries while the timeline is still zooming).
|
||||
fn elements_latency(d: &mut Driver) -> usize {
|
||||
let (req, reply) = ControlRequest::new("ui.elements", json!({"prefix": "timeline."}));
|
||||
d.tx.send(req).unwrap();
|
||||
for n in 1..=600 {
|
||||
d.frames(1);
|
||||
if let Ok(v) = reply.try_recv() {
|
||||
assert_eq!(v["ok"], true, "{v}");
|
||||
return n;
|
||||
}
|
||||
}
|
||||
panic!("ui.elements never answered");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elements_answer_promptly_once_the_timeline_is_at_rest() {
|
||||
let mut d = Driver::demo();
|
||||
d.frames(120);
|
||||
// a timeline at rest must not keep `ui.elements` waiting (the animation snaps to its target)
|
||||
assert!(elements_latency(&mut d) <= 3);
|
||||
for pps in [40.0, 400.0, 7.5] {
|
||||
d.ok("ui.set", json!({"timeline": {"pps": pps}}));
|
||||
d.ok("ui.set", json!({"timeline": {"scroll": 1.5}}));
|
||||
d.frames(120);
|
||||
let n = elements_latency(&mut d);
|
||||
assert!(n <= 3, "ui.elements took {n} frames at rest after zooming to {pps} px/s");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue