From 463a4a52217d00f2005ac865243f9c41967bbd8e Mon Sep 17 00:00:00 2001 From: FredApps Date: Mon, 12 Oct 2026 22:25:40 +0200 Subject: [PATCH 1/2] Preview: say so in the viewer and Timeline when Video is off (#1118) --- crates/ui-egui/src/lib.rs | 8 +++++ crates/ui-egui/src/panels/timeline.rs | 7 ++++ crates/ui-egui/src/panels/viewer.rs | 6 ++++ crates/ui-egui/tests/ui_video_preview_off.rs | 37 ++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 crates/ui-egui/tests/ui_video_preview_off.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 746d917b..1325dab3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1128,6 +1128,14 @@ impl EffectcraftApp { true } + /// A preview plays with Video off in the Preview panel (audio only): the current time stays + /// put, so the viewer and Timeline say so instead of looking stuck (#1118). The frame under + /// the audible sample, when one is known. + pub fn video_preview_off(&self) -> Option> { + let p = self.playback.plan.filter(|_| self.playback.playing)?; + (!p.video).then_some(self.playback.audio_frame) + } + /// The viewer shows grids, guides and safe margins (hidden while a preview without /// Include Overlays plays). pub fn overlays_visible(&self) -> bool { diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 72f10429..d4ed05d4 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1320,6 +1320,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let tc_rect = Rect::from_min_size(pos2(rect.min.x + 12.0, top + 6.0), vec2(150.0, 24.0)); let tc_resp = ui.interact(tc_rect, egui::Id::new("tl-timecode"), Sense::click_and_drag()); p.text(tc_rect.left_center(), Align2::LEFT_CENTER, &tc, Tokens::semibold(16.0), t.timecode); + // Audio-only preview (Video off in the Preview panel): the current time stays put, so the + // header says why and where the sound is (#1118). + if let Some(af) = app.video_preview_off() { + let at = af.map(|f| format!(" · {}", effectcraft_engine::time::format_timecode_ae(f + fr.frame_at(comp.display_start), fr, false))).unwrap_or_default(); + let msg = format!("{}{at}", crate::i18n::tr("Video preview off: audio only")); + p.text(pos2(tc_rect.min.x, top + 40.0), Align2::LEFT_CENTER, msg, Tokens::ui(11.0), t.cache_green); + } // Under it the other style (frames under timecode, timecode under frames) and the rate. let settings = &app.session.project.settings; let other = if settings.time_display == effectcraft_engine::project::TimeDisplayStyle::Timecode { diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index cbb842d9..316d0a60 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -1951,6 +1951,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { painter.rect_filled(r, 11.0, Color32::from_black_alpha(170)); painter.text(r.center(), Align2::CENTER_CENTER, crate::i18n::tr("Caching frames…"), Tokens::ui(11.5), t.cache_green); } + if app.video_preview_off().is_some() { + let r = Rect::from_min_size(area.min + vec2(10.0, 10.0), vec2(330.0, 22.0)); + painter.rect_filled(r, 11.0, Color32::from_black_alpha(190)); + painter.text(r.center(), Align2::CENTER_CENTER, crate::i18n::tr("Video preview is off (Preview panel): audio only"), Tokens::ui(11.5), t.timecode); + ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("viewer-video-off-chip"), r)); + } vt::draw_snap(&ctx, &painter, &map, &ectx); vt::rulers(app, ui, &map, outer, area); diff --git a/crates/ui-egui/tests/ui_video_preview_off.rs b/crates/ui-egui/tests/ui_video_preview_off.rs new file mode 100644 index 00000000..e27a37c2 --- /dev/null +++ b/crates/ui-egui/tests/ui_video_preview_off.rs @@ -0,0 +1,37 @@ +//! #1118: a preview with Video off in the Preview panel says so while it plays. + +use effectcraft_engine::Session; +use effectcraft_ui_egui::EffectcraftApp; +use egui_kittest::Harness; +use serde_json::json; + +fn frame(h: &Harness<'_, EffectcraftApp>) -> i64 { + let app = h.state(); + let c = app.session.active_comp().unwrap(); + c.frame_rate.frame_at(app.session.time()) +} + +/// #1118: with Video off in the Preview panel the preview plays audio only and the current time +/// stays put; the viewer says so while it plays, and stops saying so once it stops. +#[test] +fn an_audio_only_preview_says_video_preview_is_off() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Muted", "width": 320, "height": 180, "duration": 10})).unwrap(); + let sc = s.prefs.preview.current; + s.prefs.preview.get_mut(sc).include_video = false; + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 30.0).build_eframe(|_| EffectcraftApp::new(s)); + h.run_steps(4); + let chip = |h: &Harness<'_, EffectcraftApp>| h.ctx.data(|d| d.get_temp::(egui::Id::new("viewer-video-off-chip"))); + assert!(chip(&h).is_none()); + let now = h.ctx.input(|i| i.time); + h.state_mut().play(now); + h.run_steps(10); + assert!(h.state().video_preview_off().is_some()); + assert!(chip(&h).is_some(), "the viewer shows the Video-off notice"); + assert_eq!(frame(&h), 0, "the current time stays put with Video off"); + h.state_mut().stop(); + h.ctx.data_mut(|d| d.remove::(egui::Id::new("viewer-video-off-chip"))); + h.run_steps(3); + assert!(h.state().video_preview_off().is_none()); + assert!(chip(&h).is_none(), "no notice once stopped"); +} From b4e7f8615fc0a04163867eec7bf1c0fff0b7eb6c Mon Sep 17 00:00:00 2001 From: FredApps Date: Mon, 12 Oct 2026 23:30:14 +0200 Subject: [PATCH 2/2] Timeline: the Video-off notice takes the frames/fps line instead of overlapping it (#1118) --- crates/ui-egui/src/panels/timeline.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index d4ed05d4..d0083717 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1320,13 +1320,6 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let tc_rect = Rect::from_min_size(pos2(rect.min.x + 12.0, top + 6.0), vec2(150.0, 24.0)); let tc_resp = ui.interact(tc_rect, egui::Id::new("tl-timecode"), Sense::click_and_drag()); p.text(tc_rect.left_center(), Align2::LEFT_CENTER, &tc, Tokens::semibold(16.0), t.timecode); - // Audio-only preview (Video off in the Preview panel): the current time stays put, so the - // header says why and where the sound is (#1118). - if let Some(af) = app.video_preview_off() { - let at = af.map(|f| format!(" · {}", effectcraft_engine::time::format_timecode_ae(f + fr.frame_at(comp.display_start), fr, false))).unwrap_or_default(); - let msg = format!("{}{at}", crate::i18n::tr("Video preview off: audio only")); - p.text(pos2(tc_rect.min.x, top + 40.0), Align2::LEFT_CENTER, msg, Tokens::ui(11.0), t.cache_green); - } // Under it the other style (frames under timecode, timecode under frames) and the rate. let settings = &app.session.project.settings; let other = if settings.time_display == effectcraft_engine::project::TimeDisplayStyle::Timecode { @@ -1334,7 +1327,15 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } else { effectcraft_engine::time::format_timecode_ae(fr.frame_at(time + comp.display_start), fr, false) }; - p.text(pos2(tc_rect.min.x, tc_rect.max.y + 8.0), Align2::LEFT_CENTER, format!("{other} ({:.2} fps)", fr.as_f64()), Tokens::ui(10.5), t.text_faint); + // Audio-only preview (Video off in the Preview panel): the current time stays put, so that + // line says why and where the sound is instead (#1118). + if let Some(af) = app.video_preview_off() { + let at = af.map(|f| format!(" · {}", effectcraft_engine::time::format_timecode_ae(f + fr.frame_at(comp.display_start), fr, false))).unwrap_or_default(); + let msg = format!("{}{at}", crate::i18n::tr("Video preview off: audio only")); + p.text(pos2(tc_rect.min.x, tc_rect.max.y + 8.0), Align2::LEFT_CENTER, msg, Tokens::ui(10.5), t.cache_green); + } else { + p.text(pos2(tc_rect.min.x, tc_rect.max.y + 8.0), Align2::LEFT_CENTER, format!("{other} ({:.2} fps)", fr.as_f64()), Tokens::ui(10.5), t.text_faint); + } app.auto.add("timeline.timecode", tc_rect, &tc); super::toggle_time_display(app, &tc_resp); if tc_resp.dragged() {