git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commita66e9c57c986bc280a79f07f23f4eb84e754193a
parente6e1fa758e
authorLucas Galante <[email protected]>
date2026-09-24 16:46
feat: Ctrl+Up rewinds the playbar to the start frame and pauses it

A frame_start command in the Playbar context: stops a moving timeline in
either direction and lands the playhead on the start frame; from a stop it
is a plain jump. Ctrl+Up was unbound.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/app.rs      |  8 ++++++++
 src/command.rs  |  4 ++++
 src/main.rs     | 37 +++++++++++++++++++++++++++++++++++++
 src/shortcut.rs |  1 +
 4 files changed, 50 insertions(+)

diff --git a/src/app.rs b/src/app.rs
index 941391f..90bf050 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -6953,6 +6953,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 let pb = self.slots.playbar.inner_mut();
                 pb.current_frame = (pb.current_frame.round() + step).clamp(pb.start_frame, pb.end_frame);
             }
+            // Rewind: pause whatever is playing and land on the start frame.
+            // The scene rebuild follows from tick_frame's last_sim_frame
+            // diff, as for the steps above.
+            Action::FrameStart => {
+                let pb = self.slots.playbar.inner_mut();
+                pb.playing = false;
+                pb.current_frame = pb.start_frame;
+            }
         }
         if settings_changed {
             self.save_settings();
diff --git a/src/command.rs b/src/command.rs
index c4fb609..c6b0a29 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -208,6 +208,10 @@ pub const COMMANDS: &[Command] = &[
     Command { id: "play_pause_reverse", label: "Play / Pause Reverse", context: Context::Playbar, run: Run::Key(Action::PlayPauseReverse), default_chord: Some("Down") },
     Command { id: "frame_next", label: "Next Frame", context: Context::Playbar, run: Run::Key(Action::FrameNext), default_chord: Some("Right") },
     Command { id: "frame_prev", label: "Previous Frame", context: Context::Playbar, run: Run::Key(Action::FramePrev), default_chord: Some("Left") },
+    // Ctrl+Up rewinds: stops a moving timeline and lands on the start frame,
+    // the way a transport's stop-to-start does — one press whatever the
+    // timeline is doing.
+    Command { id: "frame_start", label: "Go To Start Frame", context: Context::Playbar, run: Run::Key(Action::FrameStart), default_chord: Some("Ctrl+Up") },
 ];
 
 pub fn by_id(id: &str) -> Option<&'static Command> {
diff --git a/src/main.rs b/src/main.rs
index 8c161f9..05edda4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1384,6 +1384,43 @@ mod tests {
         assert_eq!(m.match_command(&plain, &Key::Named(NamedKey::ArrowDown)), Some("play_pause_reverse"));
     }
 
+    /// Ctrl+Up rewinds: a moving timeline stops and the playhead lands on
+    /// the start frame, from wherever it was and in either direction; from a
+    /// stop it is simply a jump to the start.
+    #[test]
+    fn ctrl_up_rewinds_to_the_start_frame_and_pauses() {
+        use crate::command::by_id;
+        let cmd = by_id("frame_start").expect("no frame_start command");
+        assert_eq!(cmd.default_chord, Some("Ctrl+Up"));
+        assert_eq!(cmd.context, crate::command::Context::Playbar);
+
+        let mut state = State::new(false);
+        {
+            let pb = state.slots.playbar.inner_mut();
+            pb.start_frame = 1.0;
+            pb.end_frame = 48.0;
+            pb.current_frame = 17.5;
+            pb.playing = true;
+            pb.reversed = true;
+        }
+        assert!(state.run_command("frame_start"));
+        {
+            let pb = state.slots.playbar.inner();
+            assert!(!pb.playing, "a moving timeline stops");
+            assert_eq!(pb.current_frame, 1.0, "and lands on the start frame");
+        }
+        // Stopped, mid-timeline: a plain jump.
+        state.slots.playbar.inner_mut().current_frame = 30.0;
+        state.run_command("frame_start");
+        assert_eq!(state.slots.playbar.inner().current_frame, 1.0);
+        assert!(!state.slots.playbar.inner().playing);
+        // Whatever the start frame is.
+        state.slots.playbar.inner_mut().start_frame = 5.0;
+        state.slots.playbar.inner_mut().current_frame = 30.0;
+        state.run_command("frame_start");
+        assert_eq!(state.slots.playbar.inner().current_frame, 5.0);
+    }
+
     /// Either play toggle pauses a moving timeline; direction only chooses
     /// what starts from a stop — and the reverse tick runs the frame counter
     /// down, wrapping start→end.
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 1719b7c..d677b1a 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -36,6 +36,7 @@ pub enum Action {
     PlayPauseReverse,
     FrameNext,
     FramePrev,
+    FrameStart,
     Undo,
     Redo,
     /// Open the command palette — a command like any other, so it is