From 08a4b69948d9f2b8cdaf1a47caa30db7f3029aab Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 19 Mar 2025 02:15:40 -0700 Subject: [PATCH] Add animation control buttons to document bar --- editor/src/dispatcher.rs | 2 + .../messages/animation/animation_message.rs | 2 +- .../animation/animation_message_handler.rs | 53 +++++++++++++++---- .../messages/input_mapper/input_mappings.rs | 2 +- .../document/document_message_handler.rs | 15 +++++- .../portfolio/portfolio_message_handler.rs | 11 ++-- .../assets/icon-16px-solid/playback-pause.svg | 4 ++ .../assets/icon-16px-solid/playback-play.svg | 3 ++ .../icon-16px-solid/playback-to-end.svg | 4 ++ .../icon-16px-solid/playback-to-start.svg | 4 ++ frontend/src/utility-functions/icons.ts | 8 +++ 11 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 frontend/assets/icon-16px-solid/playback-pause.svg create mode 100644 frontend/assets/icon-16px-solid/playback-play.svg create mode 100644 frontend/assets/icon-16px-solid/playback-to-end.svg create mode 100644 frontend/assets/icon-16px-solid/playback-to-start.svg diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 8a3b168e..252ad14b 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -234,6 +234,7 @@ impl Dispatcher { let current_tool = &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type; let message_logging_verbosity = self.message_handlers.debug_message_handler.message_logging_verbosity; let timing_information = self.message_handlers.animation_message_handler.timing_information(); + let animation = &self.message_handlers.animation_message_handler; self.message_handlers.portfolio_message_handler.process_message( message, @@ -244,6 +245,7 @@ impl Dispatcher { current_tool, message_logging_verbosity, timing_information, + animation, }, ); } diff --git a/editor/src/messages/animation/animation_message.rs b/editor/src/messages/animation/animation_message.rs index 19dabcf9..13166800 100644 --- a/editor/src/messages/animation/animation_message.rs +++ b/editor/src/messages/animation/animation_message.rs @@ -8,7 +8,7 @@ pub enum AnimationMessage { ToggleLivePreview, EnableLivePreview, DisableLivePreview, - ResetAnimation, + RestartAnimation, SetFrameIndex(f64), SetTime(f64), UpdateTime, diff --git a/editor/src/messages/animation/animation_message_handler.rs b/editor/src/messages/animation/animation_message_handler.rs index ff1e286d..cd5e010d 100644 --- a/editor/src/messages/animation/animation_message_handler.rs +++ b/editor/src/messages/animation/animation_message_handler.rs @@ -14,6 +14,8 @@ pub enum AnimationTimeMode { #[derive(Debug, Default)] pub struct AnimationMessageHandler { live_preview: bool, + /// Used to re-send the UI on the next frame after playback starts + live_preview_recently_zero: bool, timestamp: f64, frame_index: f64, animation_start: Option, @@ -29,6 +31,10 @@ impl AnimationMessageHandler { }; TimingInformation { time: self.timestamp, animation_time } } + + pub fn is_playing(&self) -> bool { + self.live_preview + } } impl MessageHandler for AnimationMessageHandler { @@ -38,23 +44,37 @@ impl MessageHandler for AnimationMessageHandler { if self.animation_start.is_none() { self.animation_start = Some(self.timestamp); } - self.live_preview = !self.live_preview + self.live_preview = !self.live_preview; + + // Update the restart and pause/play buttons + responses.add(PortfolioMessage::UpdateDocumentWidgets); } AnimationMessage::EnableLivePreview => { if self.animation_start.is_none() { self.animation_start = Some(self.timestamp); } - self.live_preview = true + self.live_preview = true; + + // Update the restart and pause/play buttons + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } + AnimationMessage::DisableLivePreview => { + self.live_preview = false; + + // Update the restart and pause/play buttons + responses.add(PortfolioMessage::UpdateDocumentWidgets); } - AnimationMessage::DisableLivePreview => self.live_preview = false, AnimationMessage::SetFrameIndex(frame) => { self.frame_index = frame; - log::debug!("set frame index to {}", frame); - responses.add(PortfolioMessage::SubmitActiveGraphRender) + responses.add(PortfolioMessage::SubmitActiveGraphRender); + // Update the restart and pause/play buttons + responses.add(PortfolioMessage::UpdateDocumentWidgets); } AnimationMessage::SetTime(time) => { self.timestamp = time; - responses.add(AnimationMessage::UpdateTime); + if self.live_preview { + responses.add(AnimationMessage::UpdateTime); + } } AnimationMessage::IncrementFrameCounter => { if self.live_preview { @@ -64,21 +84,32 @@ impl MessageHandler for AnimationMessageHandler { } AnimationMessage::UpdateTime => { if self.live_preview { - responses.add(PortfolioMessage::SubmitActiveGraphRender) + responses.add(PortfolioMessage::SubmitActiveGraphRender); + + if self.live_preview_recently_zero { + // Update the restart and pause/play buttons + responses.add(PortfolioMessage::UpdateDocumentWidgets); + self.live_preview_recently_zero = false; + } } } - AnimationMessage::ResetAnimation => { + AnimationMessage::RestartAnimation => { self.frame_index = 0.; self.animation_start = None; - responses.add(PortfolioMessage::SubmitActiveGraphRender) + self.live_preview_recently_zero = true; + responses.add(PortfolioMessage::SubmitActiveGraphRender); + // Update the restart and pause/play buttons + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } + AnimationMessage::SetAnimationTimeMode(animation_time_mode) => { + self.animation_time_mode = animation_time_mode; } - AnimationMessage::SetAnimationTimeMode(animation_time_mode) => self.animation_time_mode = animation_time_mode, } } advertise_actions!(AnimationMessageDiscriminant; ToggleLivePreview, SetFrameIndex, - ResetAnimation, + RestartAnimation, ); } diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index b66276a2..cc83a32e 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -432,7 +432,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Digit2); modifiers=[Alt], action_dispatch=DebugMessage::MessageContents), // AnimationMessage entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview), - entry!(KeyDown(ArrowLeft); modifiers=[Control], action_dispatch=AnimationMessage::ResetAnimation), + entry!(KeyDown(Home); modifiers=[Shift], action_dispatch=AnimationMessage::RestartAnimation), ]; let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move) = mappings; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f32ae795..e5f8fbd1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -33,6 +33,7 @@ use graphene_core::raster::image::ImageFrameTable; use graphene_core::vector::style::ViewMode; use graphene_std::renderer::{ClickTarget, Quad}; use graphene_std::vector::{PointId, path_bool_lib}; +use std::time::Duration; pub struct DocumentMessageData<'a> { pub document_id: DocumentId, @@ -1988,7 +1989,7 @@ impl DocumentMessageHandler { } } - pub fn update_document_widgets(&self, responses: &mut VecDeque) { + pub fn update_document_widgets(&self, responses: &mut VecDeque, animation_is_playing: bool, time: Duration) { // Document mode (dropdown menu at the left of the bar above the viewport, before the tool options) let document_mode_layout = WidgetLayout::new(vec![LayoutGroup::Row { @@ -2026,6 +2027,18 @@ impl DocumentMessageHandler { let mut snapping_state2 = self.snapping_state.clone(); let mut widgets = vec![ + IconButton::new("PlaybackToStart", 24) + .tooltip("Restart Animation") + .tooltip_shortcut(action_keys!(AnimationMessageDiscriminant::RestartAnimation)) + .on_update(|_| AnimationMessage::RestartAnimation.into()) + .disabled(time == Duration::ZERO) + .widget_holder(), + IconButton::new(if animation_is_playing { "PlaybackPause" } else { "PlaybackPlay" }, 24) + .tooltip(if animation_is_playing { "Pause Animation" } else { "Play Animation" }) + .tooltip_shortcut(action_keys!(AnimationMessageDiscriminant::ToggleLivePreview)) + .on_update(|_| AnimationMessage::ToggleLivePreview.into()) + .widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), CheckboxInput::new(self.overlays_visible) .icon("Overlays") .tooltip("Overlays") diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index a44b5ba9..35cb4404 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -34,6 +34,7 @@ pub struct PortfolioMessageData<'a> { pub current_tool: &'a ToolType, pub message_logging_verbosity: MessageLoggingVerbosity, pub timing_information: TimingInformation, + pub animation: &'a AnimationMessageHandler, } #[derive(Debug, Default)] @@ -59,6 +60,7 @@ impl MessageHandler> for PortfolioMes current_tool, message_logging_verbosity, timing_information, + animation, } = data; match message { @@ -1108,7 +1110,7 @@ impl MessageHandler> for PortfolioMes } PortfolioMessage::UpdateDocumentWidgets => { if let Some(document) = self.active_document() { - document.update_document_widgets(responses); + document.update_document_widgets(responses, animation.is_playing(), timing_information.animation_time); } } PortfolioMessage::UpdateOpenDocumentsList => { @@ -1275,16 +1277,19 @@ impl PortfolioMessageHandler { /// Get the id of the node that should be used as the target for the spreadsheet pub fn inspect_node_id(&self) -> Option { + // Spreadsheet not open, skipping if !self.spreadsheet.spreadsheet_view_open { - warn!("Spreadsheet not open, skipping…"); return None; } + let document = self.documents.get(&self.active_document_id?)?; let selected_nodes = document.network_interface.selected_nodes().0; + + // Selected nodes != 1, skipping if selected_nodes.len() != 1 { - warn!("selected nodes != 1, skipping…"); return None; } + selected_nodes.first().copied() } } diff --git a/frontend/assets/icon-16px-solid/playback-pause.svg b/frontend/assets/icon-16px-solid/playback-pause.svg new file mode 100644 index 00000000..797ad55f --- /dev/null +++ b/frontend/assets/icon-16px-solid/playback-pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icon-16px-solid/playback-play.svg b/frontend/assets/icon-16px-solid/playback-play.svg new file mode 100644 index 00000000..2db27a90 --- /dev/null +++ b/frontend/assets/icon-16px-solid/playback-play.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/icon-16px-solid/playback-to-end.svg b/frontend/assets/icon-16px-solid/playback-to-end.svg new file mode 100644 index 00000000..79ce81f6 --- /dev/null +++ b/frontend/assets/icon-16px-solid/playback-to-end.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icon-16px-solid/playback-to-start.svg b/frontend/assets/icon-16px-solid/playback-to-start.svg new file mode 100644 index 00000000..a697311f --- /dev/null +++ b/frontend/assets/icon-16px-solid/playback-to-start.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index eb963bb9..ba0592cc 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -163,6 +163,10 @@ import PadlockUnlocked from "@graphite-frontend/assets/icon-16px-solid/padlock-u import Paste from "@graphite-frontend/assets/icon-16px-solid/paste.svg"; import PinActive from "@graphite-frontend/assets/icon-16px-solid/pin-active.svg"; import PinInactive from "@graphite-frontend/assets/icon-16px-solid/pin-inactive.svg"; +import PlaybackPause from "@graphite-frontend/assets/icon-16px-solid/playback-pause.svg"; +import PlaybackPlay from "@graphite-frontend/assets/icon-16px-solid/playback-play.svg"; +import PlaybackToEnd from "@graphite-frontend/assets/icon-16px-solid/playback-to-end.svg"; +import PlaybackToStart from "@graphite-frontend/assets/icon-16px-solid/playback-to-start.svg"; import Random from "@graphite-frontend/assets/icon-16px-solid/random.svg"; import Reload from "@graphite-frontend/assets/icon-16px-solid/reload.svg"; import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg"; @@ -279,6 +283,10 @@ const SOLID_16PX = { Paste: { svg: Paste, size: 16 }, PinActive: { svg: PinActive, size: 16 }, PinInactive: { svg: PinInactive, size: 16 }, + PlaybackPause: { svg: PlaybackPause, size: 16 }, + PlaybackPlay: { svg: PlaybackPlay, size: 16 }, + PlaybackToEnd: { svg: PlaybackToEnd, size: 16 }, + PlaybackToStart: { svg: PlaybackToStart, size: 16 }, Random: { svg: Random, size: 16 }, Reload: { svg: Reload, size: 16 }, Reset: { svg: Reset, size: 16 },