Window > Timeline panel
Floating Timeline panel driving the engine timeline.* commands: Create Video Timeline, frame counter + prev/next, playhead scrubber, fps, delete. UI stays in ui-egui (behaviour is engine commands). Parity 608 -> 609; FLOOR 604. Snapshot-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
39a014d7fc
commit
c080ed3e72
|
|
@ -22,6 +22,7 @@ pub mod comps_ui;
|
|||
pub mod control;
|
||||
pub mod dialogs;
|
||||
mod variables_ui;
|
||||
mod timeline_ui;
|
||||
pub mod doc_props_ui;
|
||||
pub mod enable_rules;
|
||||
pub mod export_dialog;
|
||||
|
|
@ -545,6 +546,7 @@ impl eframe::App for PhotocraftApp {
|
|||
preset_panels::windows(self, &ctx);
|
||||
type_panels_ui::windows(self, &ctx);
|
||||
analysis_ui::windows(self, &ctx);
|
||||
timeline_ui::windows(self, &ctx);
|
||||
workspace_ui::windows(self, &ctx);
|
||||
palette::show(self, &ctx);
|
||||
dialogs::show(self, &ctx);
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ pub fn invoke(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: Va
|
|||
if let Some(r) = crate::variables_ui::menu(app, id, ¶ms) {
|
||||
return r;
|
||||
}
|
||||
// Window > Timeline panel.
|
||||
if let Some(r) = crate::timeline_ui::menu(app, id, ¶ms) {
|
||||
return r;
|
||||
}
|
||||
if id == "window.panel.brushes" {
|
||||
// Window › Brushes opens the Brush Settings window on its presets tab.
|
||||
app.ui.panels.brush_settings = true;
|
||||
|
|
@ -404,6 +408,9 @@ fn checked(app: &PhotocraftApp, id: &str) -> Option<bool> {
|
|||
if let Some(c) = crate::preset_panels::checked(app, id) {
|
||||
return Some(c);
|
||||
}
|
||||
if let Some(c) = crate::timeline_ui::checked(app, id) {
|
||||
return Some(c);
|
||||
}
|
||||
if let Some(c) = crate::type_panels_ui::checked(app, id) {
|
||||
return Some(c);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::menu_catalog::CATALOG;
|
|||
use crate::menus::is_live;
|
||||
|
||||
/// Minimum number of live catalog items. Raise it when parity grows; never lower it.
|
||||
pub const FLOOR: usize = 603;
|
||||
pub const FLOOR: usize = 604;
|
||||
|
||||
/// One top-level menu's coverage.
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
|
|
|
|||
|
|
@ -498,6 +498,7 @@ pub struct UiState {
|
|||
/// Ruler/Count/Note tools, Measurement Log and Notes panels (see `analysis_ui`).
|
||||
#[serde(default)]
|
||||
pub analysis: crate::analysis_ui::AnalysisUi,
|
||||
pub timeline: crate::timeline_ui::TimelineUi,
|
||||
/// Slice and Slice Select tools (see `slice_ui`).
|
||||
#[serde(default)]
|
||||
pub slices: crate::slice_ui::SliceUi,
|
||||
|
|
@ -562,6 +563,7 @@ impl Default for UiState {
|
|||
presets_ui: Default::default(),
|
||||
type_panels: Default::default(),
|
||||
analysis: Default::default(),
|
||||
timeline: Default::default(),
|
||||
slices: Default::default(),
|
||||
shell: Default::default(),
|
||||
layer_filter: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
//! Window › Timeline: a floating panel that creates and drives the document timeline. All actions
|
||||
//! run the engine `timeline.*` commands, so the control channel drives it the same way.
|
||||
|
||||
use egui::{vec2, RichText};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::theme::Tokens;
|
||||
use crate::PhotocraftApp;
|
||||
|
||||
/// Timeline panel view state.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TimelineUi {
|
||||
pub open: bool,
|
||||
}
|
||||
|
||||
/// Menu checkmark for Window › Timeline.
|
||||
pub fn checked(app: &PhotocraftApp, id: &str) -> Option<bool> {
|
||||
(id == "window.panel.timeline").then_some(app.ui.timeline.open)
|
||||
}
|
||||
|
||||
/// Toggle the Timeline panel.
|
||||
pub fn menu(app: &mut PhotocraftApp, id: &str, _params: &Value) -> Option<Result<Value, String>> {
|
||||
if id == "window.panel.timeline" {
|
||||
app.ui.timeline.open = !app.ui.timeline.open;
|
||||
return Some(Ok(json!({ "open": app.ui.timeline.open })));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Render the Timeline panel.
|
||||
pub fn windows(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
if !app.ui.timeline.open {
|
||||
return;
|
||||
}
|
||||
let tl = app.session.active().and_then(|d| d.doc.timeline.clone());
|
||||
let have_doc = app.session.active().is_some();
|
||||
let mut act: Option<(&str, Value)> = None;
|
||||
let mut close = false;
|
||||
let t = Tokens::get(ctx);
|
||||
|
||||
crate::analysis_ui::panel_window(app, ctx, "timeline", "Timeline", vec2(0.0, 520.0), 660.0, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("Timeline").strong().color(t.text));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::icons::button(ui, "x", 20.0, false, "Close").clicked() {
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
if !have_doc {
|
||||
ui.weak("Open a document to use the timeline.");
|
||||
return;
|
||||
}
|
||||
match &tl {
|
||||
None => {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
if crate::widgets::primary_button(ui, "Create Video Timeline", 200.0).clicked() {
|
||||
act = Some(("timeline.create", json!({ "duration": 30, "fps": 30.0 })));
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.weak("30 frames @ 30 fps");
|
||||
});
|
||||
}
|
||||
Some(tl) => {
|
||||
ui.horizontal(|ui| {
|
||||
if crate::icons::button(ui, "chevron-left", 22.0, false, "Previous frame").clicked() {
|
||||
act = Some(("timeline.previousFrame", json!({})));
|
||||
}
|
||||
ui.label(RichText::new(format!("Frame {} / {}", tl.current + 1, tl.duration)).color(t.text).monospace());
|
||||
if crate::icons::button(ui, "chevron-right", 22.0, false, "Next frame").clicked() {
|
||||
act = Some(("timeline.nextFrame", json!({})));
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(RichText::new(format!("{:.2}s", tl.time())).color(t.text_dim).monospace());
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::icons::button(ui, "trash", 20.0, false, "Delete Timeline").clicked() {
|
||||
act = Some(("timeline.delete", json!({})));
|
||||
}
|
||||
ui.label(RichText::new(format!("{:.0} fps", tl.fps)).color(t.text_dim));
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
// Scrubber: frame playhead across the duration.
|
||||
if tl.duration > 1 {
|
||||
let mut frame = tl.current as f64;
|
||||
let resp = ui.add(
|
||||
egui::Slider::new(&mut frame, 0.0..=(tl.duration - 1) as f64)
|
||||
.integer()
|
||||
.show_value(false)
|
||||
.text("playhead"),
|
||||
);
|
||||
if resp.changed() {
|
||||
act = Some(("timeline.setFrame", json!({ "frame": frame as u64 })));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if close {
|
||||
app.ui.timeline.open = false;
|
||||
}
|
||||
if let Some((cmd, p)) = act {
|
||||
let _ = app.run(cmd, p);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue