Timeline: Play/Pause (auto-advance the playhead at the frame rate)

Play loops the playhead via timeline.setFrame at the timeline fps, using
egui time (wasm-safe, no Instant) and requesting repaints; pausing/closing
stops it. New pause icon. ui-egui tests + wasm green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 03:19:17 -04:00
parent 6417c13b69
commit 6152c0a402
3 changed files with 47 additions and 0 deletions

14
assets/icons/pause.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="14" y="4" width="4" height="16" rx="1" />
<rect x="6" y="4" width="4" height="16" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -68,6 +68,7 @@ pub static ICONS: &[(&str, &[u8])] = &[
("pentagon", include_bytes!("../../../assets/icons/pentagon.svg")),
("pipette", include_bytes!("../../../assets/icons/pipette.svg")),
("play", include_bytes!("../../../assets/icons/play.svg")),
("pause", include_bytes!("../../../assets/icons/pause.svg")),
("plus", include_bytes!("../../../assets/icons/plus.svg")),
("pointer", include_bytes!("../../../assets/icons/pointer.svg")),
("rectangle-horizontal", include_bytes!("../../../assets/icons/rectangle-horizontal.svg")),

View File

@ -12,6 +12,12 @@ use crate::PhotocraftApp;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct TimelineUi {
pub open: bool,
/// Playing back (auto-advancing the playhead at the frame rate).
#[serde(default)]
pub playing: bool,
/// egui time (seconds) of the last auto-advance.
#[serde(default, skip)]
pub last_time: f64,
}
/// Menu checkmark for Window › Timeline.
@ -42,6 +48,8 @@ pub fn windows(app: &mut PhotocraftApp, ctx: &egui::Context) {
let have_doc = app.session.active().is_some();
let mut act: Option<(&str, Value)> = None;
let mut close = false;
let mut toggle_play = false;
let playing = app.ui.timeline.playing;
let t = Tokens::get(ctx);
crate::analysis_ui::panel_window(app, ctx, "timeline", "Timeline", vec2(0.0, 520.0), 660.0, |ui| {
@ -72,6 +80,10 @@ pub fn windows(app: &mut PhotocraftApp, ctx: &egui::Context) {
}
Some(tl) => {
ui.horizontal(|ui| {
let play_icon = if playing { "pause" } else { "play" };
if crate::icons::button(ui, play_icon, 22.0, playing, if playing { "Pause" } else { "Play" }).clicked() {
toggle_play = true;
}
if crate::icons::button(ui, "chevron-left", 22.0, false, "Previous frame").clicked() {
act = Some(("timeline.previousFrame", json!({})));
}
@ -108,8 +120,28 @@ pub fn windows(app: &mut PhotocraftApp, ctx: &egui::Context) {
if close {
app.ui.timeline.open = false;
app.ui.timeline.playing = false;
}
if toggle_play {
app.ui.timeline.playing = !app.ui.timeline.playing;
app.ui.timeline.last_time = ctx.input(|i| i.time);
}
if let Some((cmd, p)) = act {
let _ = app.run(cmd, p);
}
// Playback: auto-advance the playhead at the frame rate (loops).
if app.ui.timeline.playing {
if let Some(t) = app.session.active().and_then(|d| d.doc.timeline.clone()) {
let now = ctx.input(|i| i.time);
let period = 1.0 / f64::from(t.fps.max(1.0));
if now - app.ui.timeline.last_time >= period {
app.ui.timeline.last_time = now;
let next = (t.current + 1) % t.duration.max(1);
let _ = app.run("timeline.setFrame", json!({ "frame": next as u64 }));
}
ctx.request_repaint();
} else {
app.ui.timeline.playing = false;
}
}
}