Shortcuts: scroll the Timeline, Parent column

timeline.scrollToTime (D) centres the current time in the time graph,
timeline.scrollToLayer (X) puts the topmost selected layer at the top;
both are done when the Timeline is next drawn. timeline.parentColumn
(Shift+F4) shows or hides the Parent column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pravdomil
2026-10-12 21:04:50 +02:00
co-authored by Claude Opus 5.5
parent bf53ae6094
commit f7b366197d
3 changed files with 76 additions and 0 deletions
+31
View File
@@ -53,6 +53,9 @@ pub const UI_COMMANDS: &[UiCommand] = &[
uic!("timeline.zoomIn", "Zoom In Time", [], Some("=")),
uic!("timeline.zoomOut", "Zoom Out Time", [], Some("-")),
uic!("timeline.zoomFit", "Zoom to Fit Comp", [], None),
uic!("timeline.scrollToTime", "Scroll to Current Time", [], Some("D")),
uic!("timeline.scrollToLayer", "Scroll Topmost Selected Layer to Top", [], Some("X")),
uic!("timeline.parentColumn", "Show/Hide Parent Column", [], Some("Shift+F4")),
uic!("timeline.zoomFrameToggle", "Zoom In to Frame Level / Out to the Whole Comp", [], Some(";")),
uic!("timeline.graphEditor", "Graph Editor", [], Some("Shift+F3")),
uic!("timeline.switchesModes", "Toggle Switches / Modes", [], Some("F4")),
@@ -565,6 +568,13 @@ pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: V
app.ui.viewer.pan = [0.0, 0.0];
return Ok(Value::Null);
}
"timeline.scrollToTime" => app.ui.timeline.scroll_to = Some(crate::state::ScrollTo::Time),
"timeline.scrollToLayer" => app.ui.timeline.scroll_to = Some(crate::state::ScrollTo::Layer),
"timeline.parentColumn" => {
let on = !crate::panels::timeline::column_visible(&app.ui.timeline, "parent");
crate::panels::timeline::set_column(&mut app.ui.timeline, "parent", on)?;
return Ok(json!({"parent": on}));
}
"view.actualSize" if app.ui.focused == PanelKind::Layer => {
app.ui.layer_zoom = Some(1.0 / ctx.pixels_per_point());
app.ui.layer_pan = [0.0, 0.0];
@@ -2140,11 +2150,32 @@ mod tests {
("layer.mask.selectNext", "Alt+`"),
("layer.mask.selectPrevious", "Alt+Shift+`"),
("layer.castsShadows", "Alt+Shift+C"),
("timeline.scrollToTime", "D"),
("timeline.scrollToLayer", "X"),
("timeline.parentColumn", "Shift+F4"),
] {
assert!(bindings(&app.session).into_iter().any(|x| x.2 == id && Some((x.0, x.1)) == parse_shortcut(keys)), "{id} on {keys}");
}
}
#[test]
fn timeline_keys_scroll_and_toggle_the_parent_column() {
let ctx = egui::Context::default();
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
let run = |app: &mut EffectcraftApp, id: &str| invoke(app, &ctx, id, json!({})).unwrap();
// D and X leave a scroll for the Timeline to do when it is drawn.
run(&mut app, "timeline.scrollToTime");
assert_eq!(app.ui.timeline.scroll_to, Some(crate::state::ScrollTo::Time));
run(&mut app, "timeline.scrollToLayer");
assert_eq!(app.ui.timeline.scroll_to, Some(crate::state::ScrollTo::Layer));
// Shift+F4 shows and hides the Parent column.
let parent = crate::panels::timeline::column_visible(&app.ui.timeline, "parent");
run(&mut app, "timeline.parentColumn");
assert_eq!(crate::panels::timeline::column_visible(&app.ui.timeline, "parent"), !parent);
run(&mut app, "timeline.parentColumn");
assert_eq!(crate::panels::timeline::column_visible(&app.ui.timeline, "parent"), parent);
}
#[test]
fn dispatch_uses_the_active_custom_preset() {
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
+32
View File
@@ -230,6 +230,14 @@ fn page_to_cti(start: f64, span: f64, duration: f64, cti: f64) -> f64 {
cti.min(duration - span).max(0.0)
}
/// The visible span's start (seconds) that puts `cti` in its middle, kept inside the comp.
fn centered_on(span: f64, duration: f64, cti: f64) -> f64 {
if !(span.is_finite() && duration.is_finite() && cti.is_finite()) {
return 0.0;
}
(cti - span / 2.0).min(duration - span).max(0.0)
}
/// The text of an expression being edited when its pick whip was pressed, and the selection in
/// it (characters): the pick whip's reference goes in there.
type WhipInto = (String, [usize; 2]);
@@ -1302,6 +1310,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let span = ((graph_x1 - graph_x0 - 6.0) as f64 / pps).max(0.0);
app.ui.timeline.start = page_to_cti(app.ui.timeline.start, span, comp.duration.seconds(), app.session.time().seconds());
}
let scroll_to = app.ui.timeline.scroll_to.take();
if scroll_to == Some(crate::state::ScrollTo::Time) && app.ui.timeline.pps.is_some() {
let span = ((graph_x1 - graph_x0 - 6.0) as f64 / pps).max(0.0);
app.ui.timeline.start = centered_on(span, comp.duration.seconds(), app.session.time().seconds());
}
let tm = TMap { x0: graph_x0 + 6.0, start: app.ui.timeline.start, pps };
ctx.data_mut(|d| d.insert_temp(tl_map_id(), (tm.x0, tm.start, tm.pps)));
let top = rect.min.y;
@@ -1709,6 +1722,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let rh = t.row_h;
let total_h: f32 = rows.iter().map(|r| row_height(r, rh)).sum();
let max_scroll = (total_h - rows_rect.height() + rh).max(0.0);
if scroll_to == Some(crate::state::ScrollTo::Layer) {
// The topmost selected layer, in stack order.
let sel = &app.session.state.selected_layers;
let top = comp.layers.iter().find(|l| sel.contains(&l.id)).map(|l| l.id);
if let Some(i) = rows.iter().position(|r| matches!(r.kind, RowKind::Layer) && Some(r.layer) == top) {
app.ui.timeline.scroll_y = rows.iter().take(i).map(|r| row_height(r, rh)).sum();
}
}
if ui.rect_contains_pointer(rows_rect) {
let (dy, dx, zoom) = ui.input(|i| (i.smooth_scroll_delta.y, i.smooth_scroll_delta.x, i.modifiers.alt));
// Ctrl/Cmd+wheel and a trackpad pinch reach egui as a zoom factor rather than a scroll
@@ -3984,6 +4005,17 @@ mod tests {
assert!(app.ui.timeline.reveal_props.len() > n_keys, "UU reveals modified properties too");
}
#[test]
fn scroll_to_time_centres_the_current_time_inside_the_comp() {
assert_eq!(centered_on(4.0, 10.0, 5.0), 3.0);
// Near the ends the view stops at the comp's start and end.
assert_eq!(centered_on(4.0, 10.0, 1.0), 0.0);
assert_eq!(centered_on(4.0, 10.0, 9.5), 6.0);
// A span longer than the comp, and bad numbers, start at zero.
assert_eq!(centered_on(20.0, 10.0, 5.0), 0.0);
assert_eq!(centered_on(f64::NAN, 10.0, 5.0), 0.0);
}
/// U and the other reveal shortcuts act on the selected layers only: other layers keep their
/// twirl state and their own reveal (#160).
#[test]
+13
View File
@@ -317,6 +317,15 @@ impl TimelineState {
}
}
/// Where a Timeline shortcut scrolls to.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ScrollTo {
/// D: the current time, centred in the time graph.
Time,
/// X: the topmost selected layer, at the top of the layers.
Layer,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TimelineState {
/// Visible span start (seconds) and pixels per second; None = fit comp.
@@ -385,6 +394,9 @@ pub struct TimelineState {
/// The name column shows Source Name instead of Layer Name (click its header).
#[serde(default)]
pub source_name: bool,
/// A scroll asked for by a shortcut (D, X), done when the Timeline is next drawn.
#[serde(skip)]
pub scroll_to: Option<ScrollTo>,
}
pub fn default_tl_columns() -> BTreeSet<String> {
@@ -426,6 +438,7 @@ impl Default for TimelineState {
unlinked: BTreeSet::new(),
columns: default_tl_columns(),
source_name: false,
scroll_to: None,
}
}
}